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 (
+ <>
+
+
+ >
+ );
+};
+
+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 ? (<>>) : }
+
+
+
+
+
+
+
+
+
+
+
+
+
+