From cfdb05a901386dde184c839ad6ce567691f5b54d Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 13:16:06 +0200 Subject: [PATCH 01/14] Add vendor db schema --- .../20230620103216_add_vendors/migration.sql | 134 ++++++++++++++++++ packages/database/prisma/schema.prisma | 118 +++++++++++++++ 2 files changed, 252 insertions(+) create mode 100644 packages/database/prisma/migrations/20230620103216_add_vendors/migration.sql diff --git a/packages/database/prisma/migrations/20230620103216_add_vendors/migration.sql b/packages/database/prisma/migrations/20230620103216_add_vendors/migration.sql new file mode 100644 index 000000000..f402e087a --- /dev/null +++ b/packages/database/prisma/migrations/20230620103216_add_vendors/migration.sql @@ -0,0 +1,134 @@ +-- CreateTable +CREATE TABLE "VendorNpc" ( + "id" TEXT NOT NULL, + "name_de" TEXT NOT NULL, + "name_en" TEXT NOT NULL, + "name_es" TEXT NOT NULL, + "name_fr" TEXT NOT NULL, + + CONSTRAINT "VendorNpc_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Vendor" ( + "id" TEXT NOT NULL, + + CONSTRAINT "Vendor_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorTab" ( + "id" TEXT NOT NULL, + "vendorId" TEXT NOT NULL, + "name_de" TEXT NOT NULL, + "name_en" TEXT NOT NULL, + "name_es" TEXT NOT NULL, + "name_fr" TEXT NOT NULL, + "dailyPurchaseLimit" INTEGER, + "rotation" BOOLEAN NOT NULL, + "requiresItemId" INTEGER, + "requiresAchievementId" INTEGER, + "unlock_de" TEXT, + "unlock_en" TEXT, + "unlock_es" TEXT, + "unlock_fr" TEXT, + + CONSTRAINT "VendorTab_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorOffer" ( + "id" TEXT NOT NULL, + "vendorTabId" TEXT NOT NULL, + "itemId" INTEGER NOT NULL, + "quantity" INTEGER NOT NULL, + "dailyPurchaseLimit" INTEGER, + "weeklyPurchaseLimit" INTEGER, + "characterPurchaseLimit" INTEGER, + "accountPurchaseLimit" INTEGER, + "requiresItemId" INTEGER, + "requiresAchievementId" INTEGER, + "unlock_de" TEXT, + "unlock_en" TEXT, + "unlock_es" TEXT, + "unlock_fr" TEXT, + + CONSTRAINT "VendorOffer_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VendorCost" ( + "id" TEXT NOT NULL, + "offerId" TEXT NOT NULL, + "quantity" INTEGER NOT NULL, + "itemId" INTEGER, + "currencyId" INTEGER, + + CONSTRAINT "VendorCost_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "_vendorItem" ( + "A" INTEGER NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateTable +CREATE TABLE "_vendorNpc" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_vendorItem_AB_unique" ON "_vendorItem"("A", "B"); + +-- CreateIndex +CREATE INDEX "_vendorItem_B_index" ON "_vendorItem"("B"); + +-- CreateIndex +CREATE UNIQUE INDEX "_vendorNpc_AB_unique" ON "_vendorNpc"("A", "B"); + +-- CreateIndex +CREATE INDEX "_vendorNpc_B_index" ON "_vendorNpc"("B"); + +-- AddForeignKey +ALTER TABLE "VendorTab" ADD CONSTRAINT "VendorTab_requiresItemId_fkey" FOREIGN KEY ("requiresItemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTab" ADD CONSTRAINT "VendorTab_requiresAchievementId_fkey" FOREIGN KEY ("requiresAchievementId") REFERENCES "Achievement"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorTab" ADD CONSTRAINT "VendorTab_vendorId_fkey" FOREIGN KEY ("vendorId") REFERENCES "Vendor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorOffer" ADD CONSTRAINT "VendorOffer_requiresItemId_fkey" FOREIGN KEY ("requiresItemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorOffer" ADD CONSTRAINT "VendorOffer_requiresAchievementId_fkey" FOREIGN KEY ("requiresAchievementId") REFERENCES "Achievement"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorOffer" ADD CONSTRAINT "VendorOffer_vendorTabId_fkey" FOREIGN KEY ("vendorTabId") REFERENCES "VendorTab"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorOffer" ADD CONSTRAINT "VendorOffer_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorCost" ADD CONSTRAINT "VendorCost_offerId_fkey" FOREIGN KEY ("offerId") REFERENCES "VendorOffer"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorCost" ADD CONSTRAINT "VendorCost_itemId_fkey" FOREIGN KEY ("itemId") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "VendorCost" ADD CONSTRAINT "VendorCost_currencyId_fkey" FOREIGN KEY ("currencyId") REFERENCES "Currency"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_vendorItem" ADD CONSTRAINT "_vendorItem_A_fkey" FOREIGN KEY ("A") REFERENCES "Item"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_vendorItem" ADD CONSTRAINT "_vendorItem_B_fkey" FOREIGN KEY ("B") REFERENCES "Vendor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_vendorNpc" ADD CONSTRAINT "_vendorNpc_A_fkey" FOREIGN KEY ("A") REFERENCES "Vendor"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_vendorNpc" ADD CONSTRAINT "_vendorNpc_B_fkey" FOREIGN KEY ("B") REFERENCES "VendorNpc"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/database/prisma/schema.prisma b/packages/database/prisma/schema.prisma index 0f4bfddc8..4c423b751 100644 --- a/packages/database/prisma/schema.prisma +++ b/packages/database/prisma/schema.prisma @@ -55,6 +55,12 @@ model Item { // review queue reviews Review[] @relation("itemReview") + // vendor + vendorOffer VendorOffer[] @relation("vendorOffer") + vendorCost VendorCost[] @relation("vendorCostItem") + vendors Vendor[] @relation("vendorItem") + vendorTabRequiresItem VendorTab[] @relation("vendorTabRequiresItem") + vendorOfferRequiresItem VendorOffer[] @relation("vendorOfferRequiresItem") removedFromApi Boolean @default(false) @@ -324,6 +330,10 @@ model Achievement { rewardsItem Item[] @relation(name: "rewards_item") rewardsItemIds Int[] + // vendor + vendorTabRequiresAchievement VendorTab[] @relation("vendorTabRequiresAchievement") + vendorOfferRequiresAchievement VendorOffer[] @relation("vendorOfferRequiresAchievement") + unlocks Float? removedFromApi Boolean @default(false) @@ -571,8 +581,12 @@ model Currency { order Int + // container containedIn CurrencyContent[] @relation("containedCurrency") + // vendor + vendorCost VendorCost[] @relation("vendorCostCurrency") + removedFromApi Boolean @default(false) current_de Revision @relation("current_de", fields: [currentId_de], references: [id], onDelete: Cascade) @@ -607,6 +621,110 @@ model CurrencyHistory { @@id([currencyId, revisionId]) } +model VendorNpc { + // `VendorNpc` are npcs that open a `Vendor` when interacted with. + // TODO: add location + + id String @id @default(uuid()) + + name_de String + name_en String + name_es String + name_fr String + + vendors Vendor[] @relation("vendorNpc") +} + +model Vendor { + // `Vendor` is the shop window that opens, not an npc. A vendor window can also be opened in other ways (for example by items) + + id String @id @default(uuid()) + + tabs VendorTab[] @relation("vendorTabs") + + npcs VendorNpc[] @relation("vendorNpc") + items Item[] @relation("vendorItem") +} + +model VendorTab { + id String @id @default(uuid()) + vendorId String + + name_de String + name_en String + name_es String + name_fr String + + dailyPurchaseLimit Int? + rotation Boolean + + // requirements + requiresItemId Int? + requiresItem Item? @relation("vendorTabRequiresItem", fields: [requiresItemId], references: [id], onDelete: Cascade) + + requiresAchievementId Int? + requiresAchievement Achievement? @relation("vendorTabRequiresAchievement", fields: [requiresAchievementId], references: [id], onDelete: Cascade) + + // requiresMasteryId Int? + // requiresMastery Mastery? @relation("vendorTabRequiresMastery", fields: [requiresMasteryId], references: [id], onDelete: Cascade) + + // optional unlock description + unlock_de String? + unlock_en String? + unlock_es String? + unlock_fr String? + + offers VendorOffer[] @relation("offers") + + vendor Vendor @relation("vendorTabs", fields: [vendorId], references: [id], onDelete: Cascade) +} + +model VendorOffer { + id String @id @default(uuid()) + + vendorTabId String + itemId Int + quantity Int + + dailyPurchaseLimit Int? + weeklyPurchaseLimit Int? + characterPurchaseLimit Int? + accountPurchaseLimit Int? + + requiresItemId Int? + requiresItem Item? @relation("vendorOfferRequiresItem", fields: [requiresItemId], references: [id], onDelete: Cascade) + + requiresAchievementId Int? + requiresAchievement Achievement? @relation("vendorOfferRequiresAchievement", fields: [requiresAchievementId], references: [id], onDelete: Cascade) + + // requiresMasteryId Int? + // requiresMastery Mastery? @relation("vendorOfferRequiresMastery", fields: [requiresMasteryId], references: [id], onDelete: Cascade) + + unlock_de String? + unlock_en String? + unlock_es String? + unlock_fr String? + + cost VendorCost[] @relation("cost") + + vendorTab VendorTab @relation("offers", fields: [vendorTabId], references: [id], onDelete: Cascade) + item Item @relation("vendorOffer", fields: [itemId], references: [id], onDelete: Cascade) +} + +model VendorCost { + id String @id @default(uuid()) + + offerId String + + quantity Int + itemId Int? + currencyId Int? + + offer VendorOffer @relation("cost", fields: [offerId], references: [id], onDelete: Cascade) + item Item? @relation("vendorCostItem", fields: [itemId], references: [id], onDelete: Cascade) + currency Currency? @relation("vendorCostCurrency", fields: [currencyId], references: [id], onDelete: Cascade) +} + model Job { id String @id @default(uuid()) From 3dadc8b2faae3ec0b18e46ba95dd0150daacd07b Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:08:28 +0200 Subject: [PATCH 02/14] Add vendor icon --- packages/ui/icons/index.ts | 4 +++- packages/ui/icons/vendor.svg | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 packages/ui/icons/vendor.svg diff --git a/packages/ui/icons/index.ts b/packages/ui/icons/index.ts index 7f9070b4a..0ba631a48 100644 --- a/packages/ui/icons/index.ts +++ b/packages/ui/icons/index.ts @@ -51,13 +51,14 @@ import DeveloperIcon from './developer.svg?svgr'; import AddIcon from './add.svg?svgr'; import ReviewQueueIcon from './review-queue.svg?svgr'; import DeleteIcon from './delete.svg?svgr'; +import VendorIcon from './vendor.svg?svgr'; export type IconName = 'menu' | 'gw2treasures' | 'user' | 'revision' | 'search' | 'chevronDown' | 'chatlink' | 'jobs' | 'time' | 'mount' | 'skill' | 'specialization' | 'wvw' | 'profession' | 'skin' | 'achievement' | 'item' | 'builds' | 'armorsmith' | 'artificer' | 'chef' | 'huntsman' | 'jeweler' | 'leatherworker' | 'scribe' | 'tailor' | 'weaponsmith' | 'filter' | 'filter-active' | 'shuffle' | 'achievementPoints' | 'info' | 'locale' | 'checkmark' | 'close' | 'api-status' | 'discord' | 'external' | 'external-link' | 'mastery' | 'coins' | 'upgrade-slot' | 'infusion-slot' | 'enrichment-slot' - | 'eye' | 'status' | 'unlock' | 'more' | 'developer' | 'add' | 'review-queue' | 'delete'; + | 'eye' | 'status' | 'unlock' | 'more' | 'developer' | 'add' | 'review-queue' | 'delete' | 'vendor'; type IconComponent = FunctionComponent>; @@ -114,6 +115,7 @@ export const Icons: Record = { 'add': AddIcon, 'review-queue': ReviewQueueIcon, 'delete': DeleteIcon, + 'vendor': VendorIcon, }; export type IconProp = IconName | JSX.Element; diff --git a/packages/ui/icons/vendor.svg b/packages/ui/icons/vendor.svg new file mode 100644 index 000000000..128990158 --- /dev/null +++ b/packages/ui/icons/vendor.svg @@ -0,0 +1 @@ + \ No newline at end of file From b0465fe69beb2b4b0c8c1981406ee69662d2aed8 Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:08:45 +0200 Subject: [PATCH 03/14] Add min/max/step to NumberInput --- packages/ui/components/Form/NumberInput.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/ui/components/Form/NumberInput.tsx b/packages/ui/components/Form/NumberInput.tsx index 25adb7b21..f06fc7594 100644 --- a/packages/ui/components/Form/NumberInput.tsx +++ b/packages/ui/components/Form/NumberInput.tsx @@ -2,21 +2,24 @@ import { ChangeEvent, FC, useCallback } from 'react'; import styles from './TextInput.module.css'; export interface NumberInputProps { - value?: number; + value?: number | null; defaultValue?: number; onChange?: (value: number) => void; placeholder?: string; name?: string; readOnly?: boolean; + min?: number; + max?: number; + step?: number; }; -export const NumberInput: FC = ({ value, defaultValue, onChange, placeholder, name, readOnly }) => { +export const NumberInput: FC = ({ value, defaultValue, onChange, placeholder, name, readOnly, min, max, step }) => { const handleChange = useCallback((e: ChangeEvent) => { const number = parseInt(e.target.value); onChange?.(number); }, [onChange]); return ( - + ); }; From 950954b86532b2c14d0de854e5a966cd42cd3ba0 Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:09:26 +0200 Subject: [PATCH 04/14] Add visualOnly prop to Label and add margins --- packages/ui/components/Form/Label.module.css | 4 ++++ packages/ui/components/Form/Label.tsx | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/ui/components/Form/Label.module.css b/packages/ui/components/Form/Label.module.css index 351754b1a..b0115ec18 100644 --- a/packages/ui/components/Form/Label.module.css +++ b/packages/ui/components/Form/Label.module.css @@ -9,3 +9,7 @@ display: flex; gap: 8px; } + +.label:not(:first-child) { + margin-top: 16px; +} diff --git a/packages/ui/components/Form/Label.tsx b/packages/ui/components/Form/Label.tsx index 1bd872a9b..80f301979 100644 --- a/packages/ui/components/Form/Label.tsx +++ b/packages/ui/components/Form/Label.tsx @@ -4,13 +4,16 @@ import styles from './Label.module.css'; export interface LabelProps { label: ReactNode; children: ReactNode; + visualOnly?: boolean; } -export const Label: FC = ({ label, children }) => { +export const Label: FC = ({ label, children, visualOnly }) => { + const Tag = visualOnly ? 'div' : 'label'; + return ( - + ); }; From c58f8abf4700b126c955343c3376913944727efc Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:09:34 +0200 Subject: [PATCH 05/14] Add TabList --- .../web/components/TabList/TabList.module.css | 47 +++++++++++++++++ apps/web/components/TabList/TabList.tsx | 52 +++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 apps/web/components/TabList/TabList.module.css create mode 100644 apps/web/components/TabList/TabList.tsx diff --git a/apps/web/components/TabList/TabList.module.css b/apps/web/components/TabList/TabList.module.css new file mode 100644 index 000000000..842f90ca1 --- /dev/null +++ b/apps/web/components/TabList/TabList.module.css @@ -0,0 +1,47 @@ +.tabList { + display: flex; + flex-direction: column; + flex: 1; +} + +.tabButtons { + display: flex; + gap: 8px; + align-items: stretch; + margin-bottom: 16px; +} + +.button { + position: relative; + transition: all .3s ease; +} + +.activeButton { + composes: button; +} + +.button::after { + content: ''; + position: absolute; + display: block; + bottom: -4px; + left: 16px; + right: 16px; + height: 4px; + border-radius: 2px; + background-color: var(--color-rarity, var(--color-focus)); + opacity: 0; + transition: opacity .1s ease-in; +} + +.activeButton::after { + opacity: 1; + transition: opacity .1s ease-out; +} + +.actions { + display: flex; + gap: 8px; + align-items: stretch; + margin-left: auto; +} diff --git a/apps/web/components/TabList/TabList.tsx b/apps/web/components/TabList/TabList.tsx new file mode 100644 index 000000000..be3d80d8b --- /dev/null +++ b/apps/web/components/TabList/TabList.tsx @@ -0,0 +1,52 @@ +import { Icon, IconName } from '@gw2treasures/ui'; +import { Children, FC, Key, ReactElement, ReactNode, useEffect, useState } from 'react'; +import styles from './TabList.module.css'; +import { Button } from '@gw2treasures/ui/components/Form/Button'; + +export interface TabListProps { + children: ReactElement | Array>; + actions?: ReactNode; +} + +const identity = (x: T): T => x; + +export const TabList: FC = ({ children, actions }) => { + const [activeTab, setActiveTab] = useState(() => Children.map(children, identity)[0]?.props.id); + + useEffect(() => { + const childs = Children.map(children, identity); + if(!childs.find((child) => child.props.id === activeTab)) { + setActiveTab(childs[0]?.props.id); + } + }, [activeTab, children]); + + return ( +
+
+ {Children.map(children, (tab) => ( + + ))} + {actions &&
{actions}
} +
+ +
+ {Children.map(children, (tab) => ( + tab.props.id === activeTab ? tab : null + ))} +
+
+ ); +}; + +export interface TabProps { + id: string; + title: ReactNode; + icon?: IconName; + children: ReactNode; +} + +export const Tab: FC = ({ title, icon, children }) => { + return <>{children}; +}; From 185b134551e0e05ed27838eaea300fdce7f971f1 Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:09:42 +0200 Subject: [PATCH 06/14] Add flex to CheckBox --- packages/ui/components/Form/Checkbox.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/ui/components/Form/Checkbox.module.css b/packages/ui/components/Form/Checkbox.module.css index 7cc6f32ea..9f6c4c6e5 100644 --- a/packages/ui/components/Form/Checkbox.module.css +++ b/packages/ui/components/Form/Checkbox.module.css @@ -6,6 +6,7 @@ border-radius: 2px; gap: 10px; flex-direction: row; + flex: 1; } .wrapper:hover { From c869035580ecf9650794d653081cd0ccfcaef3ee Mon Sep 17 00:00:00 2001 From: darthmaim Date: Tue, 20 Jun 2023 23:22:49 +0200 Subject: [PATCH 07/14] [WIP] Add EditVendor dialog --- .../item/[id]/_edit-vendor/EditVendor.tsx | 145 ++++++++++++++++++ .../app/[language]/item/[id]/component.tsx | 10 ++ 2 files changed, 155 insertions(+) create mode 100644 apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx diff --git a/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx b/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx new file mode 100644 index 000000000..7fb2cfec9 --- /dev/null +++ b/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx @@ -0,0 +1,145 @@ +'use client'; +import { Dialog } from '@/components/Dialog/Dialog'; +import { ItemLink } from '@/components/Item/ItemLink'; +import { SearchItemDialog } from '@/components/Item/SearchItemDialog'; +import { FlexRow } from '@/components/Layout/FlexRow'; +import { Tab, TabList, TabProps } from '@/components/TabList/TabList'; +import { Tip } from '@/components/Tip/Tip'; +import { LocalizedEntity, localizedName } from '@/lib/localizedName'; +import { WithIcon } from '@/lib/with'; +import { Item, VendorTab } from '@gw2treasures/database'; +import { Icon } from '@gw2treasures/ui'; +import { Button } from '@gw2treasures/ui/components/Form/Button'; +import { Checkbox } from '@gw2treasures/ui/components/Form/Checkbox'; +import { Label } from '@gw2treasures/ui/components/Form/Label'; +import { NumberInput } from '@gw2treasures/ui/components/Form/NumberInput'; +import { TextInput } from '@gw2treasures/ui/components/Form/TextInput'; +import { Headline } from '@gw2treasures/ui/components/Headline/Headline'; +import { Table } from '@gw2treasures/ui/components/Table/Table'; +import { TableRowButton } from '@gw2treasures/ui/components/Table/TableRowButton'; +import { FC, ReactElement, useState } from 'react'; + +export interface EditVendorProps { + // TODO: add props +} + +export const EditVendor: FC = ({ }) => { + const [dialogOpen, setDialogOpen] = useState(false); + + return ( + <> + + setDialogOpen(false)} title="Edit Vendor"> + + + + ); +}; + +export interface EditVendorDialogProps { + // TODO: add props +} + +type EditVendorTab = Omit & { + requiresItem: WithIcon> | null; +} + +const EmptyVendorTab: Omit = { + dailyPurchaseLimit: null, + name_de: '', + name_en: '', + name_es: '', + name_fr: '', + requiresItem: null, + requiresAchievementId: null, + rotation: false, + unlock_de: '', + unlock_en: '', + unlock_es: '', + unlock_fr: '', + vendorId: 'TODO', +}; + +export const EditVendorDialog: FC = ({ }) => { + const [tabs, setTabs] = useState([{ ...EmptyVendorTab, id: crypto.randomUUID() }]); + + const editTabWithId = (id: string) => (update: Partial) => { + setTabs((tabs) => tabs.map( + (t) => t.id === id ? { ...t, ...update } : t + )); + }; + + const [addItemRequirement, setAddItemRequirement] = useState(); + + return ( + <> + setTabs([...tabs, { ...EmptyVendorTab, id: crypto.randomUUID() }])}>Add tab}> + {tabs.map>((tab) => { + const edit = editTabWithId(tab.id); + + return ( + + +
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + Items + + + + Item + Cost + Limits + Requirements + + + + {}}> Add Item + +
+
+ ); + })} +
+ { addItemRequirement && editTabWithId(addItemRequirement)({ requiresItem }); setAddItemRequirement(undefined); }}/> + + ); +}; diff --git a/apps/web/app/[language]/item/[id]/component.tsx b/apps/web/app/[language]/item/[id]/component.tsx index caed5cee2..0f82863bf 100644 --- a/apps/web/app/[language]/item/[id]/component.tsx +++ b/apps/web/app/[language]/item/[id]/component.tsx @@ -37,6 +37,7 @@ import { FormatNumber } from '@/components/Format/FormatNumber'; import { EditContents } from './_edit-content/EditContents'; import { CurrencyLink } from '@/components/Currency/CurrencyLink'; import { CurrencyValue } from '@/components/Currency/CurrencyValue'; +import { EditVendor } from './_edit-vendor/EditVendor'; export interface ItemPageComponentProps { language: Language; @@ -147,6 +148,15 @@ export const ItemPageComponent: AsyncComponent = async ( )} + {(data.details?.vendor_ids?.length ?? 0) > 0 && ( + <> + Vendor +

Using this item opens a vendor.

+ + + + )} + {item.recipeOutput && item.recipeOutput.length > 0 && ( <> Crafted From From 83b3975df5f30423ee62ada53197afefb34f8002 Mon Sep 17 00:00:00 2001 From: darthmaim Date: Mon, 3 Jul 2023 13:55:11 +0200 Subject: [PATCH 08/14] Add SearchAchievementDialog --- apps/web/app/[language]/api/search/route.tsx | 2 +- .../item/[id]/_edit-vendor/EditVendor.tsx | 35 ++++++++-- .../SearchAchievementDialog.actions.ts | 25 +++++++ .../Achievement/SearchAchievementDialog.tsx | 68 +++++++++++++++++++ 4 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 apps/web/components/Achievement/SearchAchievementDialog.actions.ts create mode 100644 apps/web/components/Achievement/SearchAchievementDialog.tsx diff --git a/apps/web/app/[language]/api/search/route.tsx b/apps/web/app/[language]/api/search/route.tsx index d5dc03033..7efcb49c8 100644 --- a/apps/web/app/[language]/api/search/route.tsx +++ b/apps/web/app/[language]/api/search/route.tsx @@ -51,7 +51,7 @@ type LocalizedNameInput = { name_fr?: Prisma.StringFilter | string; } -function nameQuery(terms: string[]): LocalizedNameInput[] { +export function nameQuery(terms: string[]): LocalizedNameInput[] { const nameQueries: LocalizedNameInput[] = ['de', 'en', 'es', 'fr'].map((lang) => ({ AND: terms.map((term) => ({ [`name_${lang}`]: { contains: term, mode: 'insensitive' }})) })); diff --git a/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx b/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx index 7fb2cfec9..d1c049b86 100644 --- a/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx +++ b/apps/web/app/[language]/item/[id]/_edit-vendor/EditVendor.tsx @@ -1,4 +1,6 @@ 'use client'; +import { AchievementLink } from '@/components/Achievement/AchievementLink'; +import { SearchAchievementDialog } from '@/components/Achievement/SearchAchievementDialog'; import { Dialog } from '@/components/Dialog/Dialog'; import { ItemLink } from '@/components/Item/ItemLink'; import { SearchItemDialog } from '@/components/Item/SearchItemDialog'; @@ -7,7 +9,7 @@ import { Tab, TabList, TabProps } from '@/components/TabList/TabList'; import { Tip } from '@/components/Tip/Tip'; import { LocalizedEntity, localizedName } from '@/lib/localizedName'; import { WithIcon } from '@/lib/with'; -import { Item, VendorTab } from '@gw2treasures/database'; +import { Achievement, Item, VendorTab } from '@gw2treasures/database'; import { Icon } from '@gw2treasures/ui'; import { Button } from '@gw2treasures/ui/components/Form/Button'; import { Checkbox } from '@gw2treasures/ui/components/Form/Checkbox'; @@ -40,8 +42,9 @@ export interface EditVendorDialogProps { // TODO: add props } -type EditVendorTab = Omit & { +type EditVendorTab = Omit & { requiresItem: WithIcon> | null; + requiresAchievement: WithIcon> | null; } const EmptyVendorTab: Omit = { @@ -51,7 +54,7 @@ const EmptyVendorTab: Omit = { name_es: '', name_fr: '', requiresItem: null, - requiresAchievementId: null, + requiresAchievement: null, rotation: false, unlock_de: '', unlock_en: '', @@ -70,6 +73,7 @@ export const EditVendorDialog: FC = ({ }) => { }; const [addItemRequirement, setAddItemRequirement] = useState(); + const [addAchievementRequirement, setAddAchievementRequirement] = useState(); return ( <> @@ -111,11 +115,33 @@ export const EditVendorDialog: FC = ({ }) => { {tab.requiresItem ? (<>) : } + +
+ +
+
+ +
+
+ +
+
+ +
+