diff --git a/apps/web/app/[language]/item/[id]/component.tsx b/apps/web/app/[language]/item/[id]/component.tsx index c6018280a..e3007eb00 100644 --- a/apps/web/app/[language]/item/[id]/component.tsx +++ b/apps/web/app/[language]/item/[id]/component.tsx @@ -40,6 +40,8 @@ import { ContentChanceColumn, ContentQuantityColumn, ItemContentQuantityColumn } import type { TODO } from '@/lib/todo'; import { pageView } from '@/lib/pageView'; import { GuildUpgradeLink } from '@/components/GuildUpgrade/GuildUpgradeLink'; +import { FlexRow } from '@gw2treasures/ui/components/Layout/FlexRow'; +import { parseIcon } from '@/lib/parseIcon'; export interface ItemPageComponentProps { language: Language; @@ -74,10 +76,12 @@ export const ItemPageComponent: AsyncComponent = async ( const compareByName = compareLocalizedName(language); + const icon = parseIcon(data.icon); + return ( } @@ -94,7 +98,7 @@ export const ItemPageComponent: AsyncComponent = async ( )} Tooltip - + {item.unlocksSkinIds.length > 0 && ( <> @@ -267,7 +271,14 @@ export const ItemPageComponent: AsyncComponent = async ( - {history.revisionId !== revision.id && View} + + {history.revisionId !== revision.id && ( + + View · + Compare + + )} + ))} diff --git a/apps/web/app/[language]/item/diff/[a]/[b]/page.tsx b/apps/web/app/[language]/item/diff/[a]/[b]/page.tsx new file mode 100644 index 000000000..86d4cddf3 --- /dev/null +++ b/apps/web/app/[language]/item/diff/[a]/[b]/page.tsx @@ -0,0 +1,90 @@ +import Link from 'next/link'; +import type { Gw2Api } from 'gw2-api-types'; +import { DiffLayout, DiffLayoutHeader, DiffLayoutRow } from '@/components/Layout/DiffLayout'; +import { EntityIcon } from '@/components/Entity/EntityIcon'; +import { parseIcon } from '@/lib/parseIcon'; +import { FormatDate } from '@/components/Format/FormatDate'; +import { Notice } from '@gw2treasures/ui/components/Notice/Notice'; +import { Separator } from '@gw2treasures/ui/components/Layout/Separator'; +import { Json } from '@/components/Format/Json'; +import { notFound } from 'next/navigation'; +import { Fragment } from 'react'; +import { db } from '@/lib/prisma'; +import { remember } from '@/lib/remember'; +import { renderDefense, renderWeaponStrength } from '@/components/Item/ItemTooltip.client'; +import { createTooltip } from '@/components/Item/ItemTooltip'; +import type { Language } from '@gw2treasures/database'; + +const getRevisions = remember(60, async function getRevisions(idA: string, idB: string) { + const [a, b] = await Promise.all([ + db?.revision.findUnique({ where: { id: idA, entity: 'Item' }}), + db?.revision.findUnique({ where: { id: idB, entity: 'Item' }}), + ]); + + if(!a || !b) { + notFound(); + } + + return { a, b }; +}); + +export default async function ItemDiffPage({ params }: { params: { a: string, b: string, language: Language }}) { + const idA = params.a.toString(); + const idB = params.b.toString(); + + const { a, b } = await getRevisions(idA, idB); + + const dataA: Gw2Api.Item = JSON.parse(a.data); + const dataB: Gw2Api.Item = JSON.parse(b.data); + + const iconA = parseIcon(dataA.icon); + const iconB = parseIcon(dataB.icon); + + const tooltipA = await createTooltip(dataA, params.language); + const tooltipB = await createTooltip(dataB, params.language); + + return ( + + , + iconB && , + ]} title={[ + dataA.name, + dataB.name, + ]} subtitle={[ + (Build {a.buildId}) ▪ View revision, + (Build {b.buildId}) ▪ View revision, + ]}/> + + {dataA.id !== dataB.id && ( +
+ You are comparing two different items +
+ )} + + {a.createdAt > b.createdAt && ( +
+ You are comparing an old version against a newer version. Switch around +
+ )} + + + + + {(tooltipA.attributes || tooltipB.attributes) && ( + [...Array(Math.max(tooltipA.attributes?.length ?? 0, tooltipB.attributes?.length ?? 0)).keys()].map((_, i) => ( + + )) + )} + + {/* } right={}/> */} + + } right={}/> + } right={} changed/> +
+ ); +}; + diff --git a/apps/web/components/Item/ItemLinkTooltip.tsx b/apps/web/components/Item/ItemLinkTooltip.tsx index 2976517f2..31afbc048 100644 --- a/apps/web/components/Item/ItemLinkTooltip.tsx +++ b/apps/web/components/Item/ItemLinkTooltip.tsx @@ -30,13 +30,16 @@ export const ItemLinkTooltip: FC = ({ item, language, revi return (
-
- {item.icon && ()} - {localizedName(item, language)} -
- Error}> - {tooltip.loading &&

} + {tooltip.loading && ( + <> +
+ {item.icon && ()} + {localizedName(item, language)} +
+

+ + )} {!tooltip.loading && }
diff --git a/apps/web/components/Item/ItemTooltip.client.tsx b/apps/web/components/Item/ItemTooltip.client.tsx index 3e94a1fbd..adf1cd7f2 100644 --- a/apps/web/components/Item/ItemTooltip.client.tsx +++ b/apps/web/components/Item/ItemTooltip.client.tsx @@ -1,4 +1,4 @@ -import { type FC, Fragment } from 'react'; +import { type FC, Fragment, type ReactNode } from 'react'; import { FormatNumber } from '../Format/FormatNumber'; import { ItemTooltip } from './ItemTooltip'; import { Rarity } from './Rarity'; @@ -10,14 +10,14 @@ import { Icon } from '@gw2treasures/ui'; import { EntityIcon } from '@/components/Entity/EntityIcon'; import { DyeColor } from '../Color/DyeColor'; import { hexToRgb } from '../Color/hex-to-rgb'; -import { FlexRow } from '@gw2treasures/ui/components/Layout/FlexRow'; import { Tip } from '@gw2treasures/ui/components/Tip/Tip'; export interface ClientItemTooltipProps { tooltip: ItemTooltip; + hideTitle?: boolean; }; -function renderAttributes(attributes: ItemTooltip['attributes']) { +export function renderAttributes(attributes: ItemTooltip['attributes']) { if(!attributes) { return; } @@ -97,10 +97,30 @@ function renderConsumable(consumable: ItemTooltip['consumable']) { ); } -export const ClientItemTooltip: FC = ({ tooltip }) => { - const data = [ - tooltip.weaponStrength && (<>{tooltip.weaponStrength.label}: ), - tooltip.defense && <>{tooltip.defense.label}: , +export const ClientItemTooltip: FC = ({ tooltip, hideTitle = false }) => { + const data = renderItemTooltipRows(tooltip); + + return ( +
+ {!hideTitle && ( +
+ {tooltip.icon && ()} + {tooltip.name} +
+ )} + + {data.filter(isTruthy).map((content, index) => { + // eslint-disable-next-line react/no-array-index-key + return
{content}
; + })} +
+ ); +}; + +export function renderItemTooltipRows(tooltip: ItemTooltip): ReactNode[] { + return [ + renderWeaponStrength(tooltip), + renderDefense(tooltip), renderAttributes(tooltip.attributes), tooltip.buff && (

), renderConsumable(tooltip.consumable), @@ -155,13 +175,26 @@ export const ClientItemTooltip: FC = ({ tooltip }) => { ...tooltip.flags, tooltip.value && (), ]; +} + +export function renderWeaponStrength({ weaponStrength }: ItemTooltip) { + if(!weaponStrength) { + return; + } return ( -

- {data.filter(isTruthy).map((content, index) => { - // eslint-disable-next-line react/no-array-index-key - return
{content}
; - })} -
+ <> + {weaponStrength.label}: + ); -}; +} + +export function renderDefense({ defense }: ItemTooltip) { + if(!defense) { + return; + } + + return ( + <>{defense.label}: + ); +} diff --git a/apps/web/components/Item/ItemTooltip.module.css b/apps/web/components/Item/ItemTooltip.module.css index efdabf7d3..ad92c067d 100644 --- a/apps/web/components/Item/ItemTooltip.module.css +++ b/apps/web/components/Item/ItemTooltip.module.css @@ -1,3 +1,13 @@ +.title { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 8px; + + font-family: var(--font-bitter); + color: var(--color-rarity); +} + .row + .row { margin-top: 8px; } diff --git a/apps/web/components/Item/ItemTooltip.tsx b/apps/web/components/Item/ItemTooltip.tsx index 19488f431..d5f2f16d5 100644 --- a/apps/web/components/Item/ItemTooltip.tsx +++ b/apps/web/components/Item/ItemTooltip.tsx @@ -16,13 +16,14 @@ import type { RGB } from '../Color/types'; export interface ItemTooltipProps { item: Gw2Api.Item; language: Language; + hideTitle?: boolean; } -export const ItemTooltip: AsyncComponent = async ({ item, language }) => { +export const ItemTooltip: AsyncComponent = async ({ item, language, hideTitle }) => { const tooltip = await createTooltip(item, language); return ( - + ); }; @@ -65,8 +66,12 @@ export async function createTooltip(item: Gw2Api.Item, language: Language): Prom ? await db.color.findUnique({ where: { id: item.details.color_id }}) : null; + const icon = parseIcon(item.icon); + return { language, + name: item.name, + icon, weaponStrength: item.type === 'Weapon' ? { label: 'Strength', min: item.details?.min_power ?? 0, max: item.details?.max_power ?? 0 } : undefined, defense: item.type === 'Armor' ? { label: 'Defense', value: item.details?.defense ?? 0 } : undefined, attributes: item.details?.infix_upgrade?.attributes && item.details.infix_upgrade.attributes.length > 0 ? item.details.infix_upgrade.attributes.map((({ attribute, modifier }) => ({ label: t(`attribute.${attribute}`), value: modifier }))) : undefined, @@ -121,6 +126,8 @@ export type ItemWithAttributes = WithIcon & { export interface ItemTooltip { language: Language, + name: string, + icon?: { id: number, signature: string }, weaponStrength?: { label: string, min: number, max: number }, defense?: { label: string, value: number }, attributes?: { label: string, value: number }[], diff --git a/apps/web/components/Layout/DiffLayout.tsx b/apps/web/components/Layout/DiffLayout.tsx index 843d5b2d8..077e20595 100644 --- a/apps/web/components/Layout/DiffLayout.tsx +++ b/apps/web/components/Layout/DiffLayout.tsx @@ -45,6 +45,10 @@ interface DiffLayoutRowProps { }; export const DiffLayoutRow: FC = ({ left, right, changed = false }) => { + if(!left && !right) { + return null; + } + return (
{left}