From b79c5948d006644ee244a6bc19a12cc4f09f383c Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 10 Dec 2023 23:50:10 -0500 Subject: [PATCH 1/9] feat(homebrew): Started adding basic homebrew page --- package-lock.json | 72 ++++++++++++++++ package.json | 2 + .../homebrew/deleteHomebrewExpansion.ts | 22 +++++ .../homebrew/updateHomebrewExpansion.ts | 17 ++++ .../AboutSection/AboutSection.tsx | 76 +++++++++++++++++ .../HomebrewEditorPage/AboutSection/index.ts | 1 + .../HomebrewEditorPage/HomebrewEditorPage.tsx | 85 ++++++++++++++++++- .../CreateExpansionDialog.tsx | 7 +- src/providers/AppProviders.tsx | 26 +++--- src/stores/homebrew/homebrew.slice.ts | 8 ++ src/stores/homebrew/homebrew.slice.type.ts | 5 ++ src/types/HomebrewCollection.type.ts | 3 +- 12 files changed, 307 insertions(+), 17 deletions(-) create mode 100644 src/api-calls/homebrew/deleteHomebrewExpansion.ts create mode 100644 src/api-calls/homebrew/updateHomebrewExpansion.ts create mode 100644 src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/AboutSection/index.ts diff --git a/package-lock.json b/package-lock.json index 83e9ea49..2e198744 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "@mui/icons-material": "^5.14.15", "@mui/lab": "^5.0.0-alpha.150", "@mui/material": "^5.14.15", + "@mui/x-date-pickers": "^6.18.4", "@tiptap/extension-collaboration": "^2.0.3", "@tiptap/extension-collaboration-cursor": "^2.0.3", "@tiptap/extension-placeholder": "^2.0.0-beta.218", @@ -25,6 +26,7 @@ "@tiptap/react": "^2.0.0-beta.218", "@tiptap/starter-kit": "^2.0.0-beta.218", "dataforged": "^2.0.0-2", + "dayjs": "^1.11.5", "firebase": "^9.17.1", "formik": "^2.2.9", "immer": "^9.0.17", @@ -2021,6 +2023,71 @@ "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" }, + "node_modules/@mui/x-date-pickers": { + "version": "6.18.4", + "resolved": "https://registry.npmjs.org/@mui/x-date-pickers/-/x-date-pickers-6.18.4.tgz", + "integrity": "sha512-YqJ6lxZHBIt344B3bvRAVbdYSQz4dcmJQXGcfvJTn26VdKjpgzjAqwhlbQhbAt55audJOWzGB99ImuQuljDROA==", + "dependencies": { + "@babel/runtime": "^7.23.2", + "@mui/base": "^5.0.0-beta.22", + "@mui/utils": "^5.14.16", + "@types/react-transition-group": "^4.4.8", + "clsx": "^2.0.0", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/mui" + }, + "peerDependencies": { + "@emotion/react": "^11.9.0", + "@emotion/styled": "^11.8.1", + "@mui/material": "^5.8.6", + "@mui/system": "^5.8.0", + "date-fns": "^2.25.0", + "date-fns-jalali": "^2.13.0-0", + "dayjs": "^1.10.7", + "luxon": "^3.0.2", + "moment": "^2.29.4", + "moment-hijri": "^2.1.2", + "moment-jalaali": "^0.7.4 || ^0.8.0 || ^0.9.0 || ^0.10.0", + "react": "^17.0.0 || ^18.0.0", + "react-dom": "^17.0.0 || ^18.0.0" + }, + "peerDependenciesMeta": { + "@emotion/react": { + "optional": true + }, + "@emotion/styled": { + "optional": true + }, + "date-fns": { + "optional": true + }, + "date-fns-jalali": { + "optional": true + }, + "dayjs": { + "optional": true + }, + "luxon": { + "optional": true + }, + "moment": { + "optional": true + }, + "moment-hijri": { + "optional": true + }, + "moment-jalaali": { + "optional": true + } + } + }, "node_modules/@mui/x-tree-view": { "version": "6.0.0-alpha.1", "resolved": "https://registry.npmjs.org/@mui/x-tree-view/-/x-tree-view-6.0.0-alpha.1.tgz", @@ -3855,6 +3922,11 @@ "resolved": "https://registry.npmjs.org/dataforged/-/dataforged-2.0.0-2.tgz", "integrity": "sha512-m06t3Ul09T6+VhZmx1Y+7ExUlkHsfZ1vcTEJwPFXHtVZKl/wrCp71j9lGf1lYuCZnr1+UWpMNEe5SSdCj53bXw==" }, + "node_modules/dayjs": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.5.tgz", + "integrity": "sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA==" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", diff --git a/package.json b/package.json index d651fa2e..599e860c 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@mui/icons-material": "^5.14.15", "@mui/lab": "^5.0.0-alpha.150", "@mui/material": "^5.14.15", + "@mui/x-date-pickers": "^6.18.4", "@tiptap/extension-collaboration": "^2.0.3", "@tiptap/extension-collaboration-cursor": "^2.0.3", "@tiptap/extension-placeholder": "^2.0.0-beta.218", @@ -27,6 +28,7 @@ "@tiptap/react": "^2.0.0-beta.218", "@tiptap/starter-kit": "^2.0.0-beta.218", "dataforged": "^2.0.0-2", + "dayjs": "^1.11.5", "firebase": "^9.17.1", "formik": "^2.2.9", "immer": "^9.0.17", diff --git a/src/api-calls/homebrew/deleteHomebrewExpansion.ts b/src/api-calls/homebrew/deleteHomebrewExpansion.ts new file mode 100644 index 00000000..765ff2ce --- /dev/null +++ b/src/api-calls/homebrew/deleteHomebrewExpansion.ts @@ -0,0 +1,22 @@ +import { deleteDoc } from "firebase/firestore"; +import { getHomebrewCollectionDoc } from "./_getRef"; +import { createApiFunction } from "api-calls/createApiFunction"; + +export const deleteHomebrewExpansion = createApiFunction<{ id: string }, void>( + (params) => { + const { id } = params; + + return new Promise((resolve, reject) => { + const promises: Promise[] = []; + + promises.push(deleteDoc(getHomebrewCollectionDoc(id))); + + Promise.all(promises) + .then(() => { + resolve(); + }) + .catch(reject); + }); + }, + "Failed to delete expansion." +); diff --git a/src/api-calls/homebrew/updateHomebrewExpansion.ts b/src/api-calls/homebrew/updateHomebrewExpansion.ts new file mode 100644 index 00000000..c73763d0 --- /dev/null +++ b/src/api-calls/homebrew/updateHomebrewExpansion.ts @@ -0,0 +1,17 @@ +import { updateDoc } from "firebase/firestore"; +import { getHomebrewCollectionDoc } from "./_getRef"; +import { BaseExpansion } from "types/HomebrewCollection.type"; +import { createApiFunction } from "api-calls/createApiFunction"; + +export const updateHomebrewExpansion = createApiFunction< + { id: string; expansion: Partial }, + void +>((params) => { + const { id, expansion } = params; + + return new Promise((resolve, reject) => { + updateDoc(getHomebrewCollectionDoc(id), expansion) + .then(() => resolve()) + .catch(reject); + }); +}, "Failed to update expansion."); diff --git a/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx new file mode 100644 index 00000000..51cae752 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx @@ -0,0 +1,76 @@ +import { DatePicker } from "@mui/x-date-pickers"; +import { Box, Grid, TextField } from "@mui/material"; +import { SectionHeading } from "components/shared/SectionHeading"; +import { useEffect, useState } from "react"; +import { useStore } from "stores/store"; + +export interface AboutSectionProps { + id: string; +} + +export function AboutSection(props: AboutSectionProps) { + const { id } = props; + + const details = useStore((store) => store.homebrew.collections[id]); + const updateDetails = useStore((store) => store.homebrew.updateExpansion); + + const originalTitle = details.title; + const [title, setTitle] = useState(details.title ?? ""); + + useEffect(() => { + if (originalTitle) { + setTitle(originalTitle); + } + }, [originalTitle]); + + return ( + :not(:last-of-type)"]: { + mb: 2, + }, + }} + > + + + + setTitle(evt.currentTarget.value)} + onBlur={(evt) => + updateDetails(id, { title: evt.currentTarget.value }).catch( + () => {} + ) + } + fullWidth + /> + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/AboutSection/index.ts b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/index.ts new file mode 100644 index 00000000..1c17ed01 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/index.ts @@ -0,0 +1 @@ +export * from "./AboutSection"; diff --git a/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx b/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx index 974d4395..0069ce27 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx @@ -1,8 +1,17 @@ -import { Box } from "@mui/material"; +import { Box, Button, LinearProgress } from "@mui/material"; import { PageContent, PageHeader } from "components/shared/Layout"; import { StyledTab, StyledTabs } from "components/shared/StyledTabs"; -import { useState } from "react"; -import { useSearchParams } from "react-router-dom"; +import { useEffect, useState } from "react"; +import { + Link, + useNavigate, + useParams, + useSearchParams, +} from "react-router-dom"; +import { AboutSection } from "./AboutSection"; +import { useStore } from "stores/store"; +import { EmptyState } from "components/shared/EmptyState"; +import { BASE_ROUTES, basePaths } from "routes"; enum TABS { ABOUT = "about", @@ -13,6 +22,18 @@ enum TABS { } export function HomebrewEditorPage() { + const { homebrewId } = useParams(); + + const navigate = useNavigate(); + + const loading = useStore((store) => store.homebrew.loading); + const homebrewName = useStore((store) => + homebrewId && store.homebrew.collections[homebrewId] + ? store.homebrew.collections[homebrewId].title ?? "Unnamed Collection" + : undefined + ); + const deleteCollection = useStore((store) => store.homebrew.deleteExpansion); + const [searchParams, setSearchParams] = useSearchParams(); const [selectedTab, setSelectedTab] = useState( (searchParams.get("tab") as TABS) ?? TABS.ABOUT @@ -23,9 +44,62 @@ export function HomebrewEditorPage() { setSearchParams({ tab }); }; + const [syncLoading, setSyncLoading] = useState(true); + + useEffect(() => { + const timeout = setTimeout(() => { + setSyncLoading(false); + }, 2 * 1000); + + return () => { + clearTimeout(timeout); + }; + }, []); + + if (loading || (!homebrewName && syncLoading)) { + return ; + } + if (!homebrewId || !homebrewName) { + return ( + + Your Homebrew + + } + /> + ); + } + return ( <> - + + deleteCollection(homebrewId) + .then(() => { + navigate(basePaths[BASE_ROUTES.HOMEBREW]); + }) + .catch(() => {}) + } + > + Delete Collection + + } + /> + + {selectedTab === TABS.ABOUT && } + diff --git a/src/pages/Homebrew/HomebrewSelectPage/CreateExpansionDialog.tsx b/src/pages/Homebrew/HomebrewSelectPage/CreateExpansionDialog.tsx index 4a7237f5..8141bc4b 100644 --- a/src/pages/Homebrew/HomebrewSelectPage/CreateExpansionDialog.tsx +++ b/src/pages/Homebrew/HomebrewSelectPage/CreateExpansionDialog.tsx @@ -11,8 +11,10 @@ import { dataswornVersion } from "config/datasworn.config"; import { convertIdPart } from "functions/dataswornIdEncoder"; import { useGameSystemValue } from "hooks/useGameSystemValue"; import { useState } from "react"; +import { useNavigate } from "react-router-dom"; import { useStore } from "stores/store"; import { GAME_SYSTEMS } from "types/GameSystems.type"; +import { constructHomebrewEditorPath } from "../routes"; export interface CreateExpansionDialogProps { open: boolean; @@ -23,6 +25,8 @@ export interface CreateExpansionDialogProps { export function CreateExpansionDialog(props: CreateExpansionDialogProps) { const { open, onClose, ids } = props; + const navigate = useNavigate(); + const [collectionName, setCollectionName] = useState(""); const [error, setError] = useState(); @@ -58,8 +62,9 @@ export function CreateExpansionDialog(props: CreateExpansionDialogProps) { uids: [uid], title: collectionName, }) - .then(() => { + .then((id) => { onClose(); + navigate(constructHomebrewEditorPath(id)); }) .catch(() => {}); }; diff --git a/src/providers/AppProviders.tsx b/src/providers/AppProviders.tsx index 6fd0d754..3e14a921 100644 --- a/src/providers/AppProviders.tsx +++ b/src/providers/AppProviders.tsx @@ -3,20 +3,24 @@ import { ThemeProvider } from "./ThemeProvider"; import { SnackbarProvider } from "./SnackbarProvider"; import { ConfirmProvider } from "material-ui-confirm"; import { AnalyticsProvider } from "lib/analytics.lib"; +import { LocalizationProvider } from "@mui/x-date-pickers"; +import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; export function AppProviders(props: PropsWithChildren) { const { children } = props; return ( - - - - - <>{children} - - - - + + + + + + <>{children} + + + + + ); } diff --git a/src/stores/homebrew/homebrew.slice.ts b/src/stores/homebrew/homebrew.slice.ts index 0895ed76..091644fa 100644 --- a/src/stores/homebrew/homebrew.slice.ts +++ b/src/stores/homebrew/homebrew.slice.ts @@ -4,6 +4,8 @@ import { defaultHomebrewSlice } from "./homebrew.slice.default"; import { listenToHomebrewCollections } from "api-calls/homebrew/listenToHomebrewCollections"; import { getErrorMessage } from "functions/getErrorMessage"; import { createHomebrewExpansion } from "api-calls/homebrew/createHomebrewExpansion"; +import { updateHomebrewExpansion } from "api-calls/homebrew/updateHomebrewExpansion"; +import { deleteHomebrewExpansion } from "api-calls/homebrew/deleteHomebrewExpansion"; export const createHomebrewSlice: CreateSliceType = (set) => ({ ...defaultHomebrewSlice, @@ -43,4 +45,10 @@ export const createHomebrewSlice: CreateSliceType = (set) => ({ createExpansion: (expansion) => { return createHomebrewExpansion(expansion); }, + updateExpansion: (id, expansion) => { + return updateHomebrewExpansion({ id, expansion }); + }, + deleteExpansion: (id) => { + return deleteHomebrewExpansion({ id }); + }, }); diff --git a/src/stores/homebrew/homebrew.slice.type.ts b/src/stores/homebrew/homebrew.slice.type.ts index 3f374a96..e91f923c 100644 --- a/src/stores/homebrew/homebrew.slice.type.ts +++ b/src/stores/homebrew/homebrew.slice.type.ts @@ -14,6 +14,11 @@ export interface HomebrewSliceActions { subscribe: (uid: string) => Unsubscribe; createExpansion: (expansion: BaseExpansion) => Promise; + updateExpansion: ( + expansionId: string, + expansion: Partial + ) => Promise; + deleteExpansion: (expansionId: string) => Promise; } export type HomebrewSlice = HomebrewSliceData & HomebrewSliceActions; diff --git a/src/types/HomebrewCollection.type.ts b/src/types/HomebrewCollection.type.ts index 15918405..95de8c88 100644 --- a/src/types/HomebrewCollection.type.ts +++ b/src/types/HomebrewCollection.type.ts @@ -10,7 +10,8 @@ type RuleKeys = | "rarities" | "delve_sites" | "site_themes" - | "site_domains"; + | "site_domains" + | "rules"; type additions = { uids: string[]; From 29d40cd8bb023cefd1f3716f154c20d877e7005d Mon Sep 17 00:00:00 2001 From: Scott Benton Date: Tue, 12 Dec 2023 20:31:04 -0500 Subject: [PATCH 2/9] feat(homebrew): Continued work on rules section --- src/api-calls/homebrew/rules/_getRef.ts | 24 +++++++++++++++++ .../homebrew/rules/createHomebrewRules.ts | 14 ++++++++++ .../AboutSection/AboutSection.tsx | 12 ++++++--- .../HomebrewEditorPage/HomebrewEditorPage.tsx | 2 ++ .../RulesSection/RulesSection.tsx | 27 +++++++++++++++++++ .../HomebrewEditorPage/RulesSection/index.ts | 1 + src/types/HomebrewCollection.type.ts | 27 ++++++++++++++++++- 7 files changed, 102 insertions(+), 5 deletions(-) create mode 100644 src/api-calls/homebrew/rules/_getRef.ts create mode 100644 src/api-calls/homebrew/rules/createHomebrewRules.ts create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/index.ts diff --git a/src/api-calls/homebrew/rules/_getRef.ts b/src/api-calls/homebrew/rules/_getRef.ts new file mode 100644 index 00000000..9cd8984a --- /dev/null +++ b/src/api-calls/homebrew/rules/_getRef.ts @@ -0,0 +1,24 @@ +import { DocumentReference, doc } from "firebase/firestore"; +import { constructHomebrewCollectionDocPath } from "../_getRef"; +import { firestore } from "config/firebase.config"; +import { StoredRules } from "types/HomebrewCollection.type"; +import { Rules } from "@datasworn/core"; + +export function constructHomebrewRulesDocPath(homebrewId: string) { + return `${constructHomebrewCollectionDocPath(homebrewId)}/rules/rules`; +} + +export function getHomebrewRulesDoc(homebrewId: string) { + return doc( + firestore, + constructHomebrewRulesDocPath(homebrewId) + ) as DocumentReference>; +} + +export type ReplaceRecord = { + [P in keyof T]: T[P] extends Record + ? { [key in A]: B } + : T[P] extends object + ? ReplaceRecord + : T[P]; +}; diff --git a/src/api-calls/homebrew/rules/createHomebrewRules.ts b/src/api-calls/homebrew/rules/createHomebrewRules.ts new file mode 100644 index 00000000..b69e66ef --- /dev/null +++ b/src/api-calls/homebrew/rules/createHomebrewRules.ts @@ -0,0 +1,14 @@ +import { Rules } from "@datasworn/core"; +import { createApiFunction } from "api-calls/createApiFunction"; + +export const updateHomebrewRules = createApiFunction< + { + homebrewId: string; + rules: Rules; + }, + void +>(() => { + return new Promise((resolve, reject) => { + resolve(); + }); +}, "Failed to update rules."); diff --git a/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx index 51cae752..330b7242 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/AboutSection/AboutSection.tsx @@ -1,8 +1,8 @@ -import { DatePicker } from "@mui/x-date-pickers"; -import { Box, Grid, TextField } from "@mui/material"; +import { Box, Grid, TextField, Typography } from "@mui/material"; import { SectionHeading } from "components/shared/SectionHeading"; import { useEffect, useState } from "react"; import { useStore } from "stores/store"; +import { dataswornVersion } from "config/datasworn.config"; export interface AboutSectionProps { id: string; @@ -46,8 +46,12 @@ export function AboutSection(props: AboutSectionProps) { fullWidth /> + + Datasworn Version + {dataswornVersion} + - + {/* @@ -70,7 +74,7 @@ export function AboutSection(props: AboutSectionProps) { - + */} ); } diff --git a/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx b/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx index 0069ce27..78a4dee5 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/HomebrewEditorPage.tsx @@ -12,6 +12,7 @@ import { AboutSection } from "./AboutSection"; import { useStore } from "stores/store"; import { EmptyState } from "components/shared/EmptyState"; import { BASE_ROUTES, basePaths } from "routes"; +import { RulesSection } from "./RulesSection"; enum TABS { ABOUT = "about", @@ -118,6 +119,7 @@ export function HomebrewEditorPage() { {selectedTab === TABS.ABOUT && } + {selectedTab === TABS.RULES && } diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx new file mode 100644 index 00000000..5fb3b498 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx @@ -0,0 +1,27 @@ +import { Box } from "@mui/material"; +import { SectionHeading } from "components/shared/SectionHeading"; + +export interface RulesSectionProps { + id: string; +} + +export function RulesSection(props: RulesSectionProps) { + const { id } = props; + + console.debug(id); + + return ( + :not(:last-of-type)"]: { + mb: 2, + }, + }} + > + + + + + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/index.ts b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/index.ts new file mode 100644 index 00000000..77af4d94 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/index.ts @@ -0,0 +1 @@ +export * from "./RulesSection"; diff --git a/src/types/HomebrewCollection.type.ts b/src/types/HomebrewCollection.type.ts index 95de8c88..44c31bf7 100644 --- a/src/types/HomebrewCollection.type.ts +++ b/src/types/HomebrewCollection.type.ts @@ -1,4 +1,11 @@ -import { Expansion, Ruleset } from "@datasworn/core"; +import { + ConditionMeterRule, + Expansion, + ImpactCategory, + Ruleset, + SpecialTrackRule, + StatRule, +} from "@datasworn/core"; type RuleKeys = | "oracles" @@ -22,3 +29,21 @@ export type BaseExpansion = Omit & additions; export type BaseRuleset = Omit & additions; export type BaseExpansionOrRuleset = BaseExpansion | BaseRuleset; + +export interface StoredRules { + stats: { + [statKey: string]: { + label: string; + description: string; + }; + }; + condition_meters: { + [conditionMeterKey: string]: ConditionMeterRule; + }; + impacts: { + [impactKey: string]: ImpactCategory; + }; + specialTracks: { + [trackKey: string]: SpecialTrackRule; + }; +} From a830678b220784c9d65c22bfa46a937af9603c14 Mon Sep 17 00:00:00 2001 From: Scott Date: Thu, 14 Dec 2023 22:39:26 -0500 Subject: [PATCH 3/9] feat(rules): Continued working on rules section --- firestore.rules | 10 + package-lock.json | 381 +++++++----------- package.json | 4 +- src/api-calls/assets/updateAssetCheckbox.ts | 1 - src/api-calls/assets/updateAssetCondition.ts | 1 - src/api-calls/assets/updateAssetInput.ts | 1 - src/api-calls/game-log/updateLog.ts | 12 +- src/api-calls/homebrew/rules/_getRef.ts | 11 +- .../homebrew/rules/createHomebrewRules.ts | 15 +- .../homebrew/rules/listenToHomebrewRules.ts | 26 ++ .../homebrew/rules/updateHomebrewRules.ts | 21 + .../nav/NavRailFlyouts/CampaignMenu.tsx | 5 +- .../nav/NavRailFlyouts/HomebrewMenu.tsx | 2 +- .../CampaignGMScreenPage.tsx | 25 +- .../CampaignSheetPage/CampaignSheetPage.tsx | 23 +- .../CharacterSheetPage/CharacterSheetPage.tsx | 8 +- src/pages/Home/ExampleStatsSection.tsx | 1 - .../AboutSection/AboutSection.tsx | 8 +- .../HomebrewEditorPage/HomebrewEditorPage.tsx | 6 +- .../RulesSection/RulesSection.tsx | 18 +- .../HomebrewEditorPage/RulesSection/Stats.tsx | 85 ++++ .../HomebrewSelectPage/HomebrewSelectPage.tsx | 8 +- .../WorldSelectPage/components/WorldCard.tsx | 2 +- .../World/WorldSheetPage/WorldSheetPage.tsx | 20 +- src/stores/homebrew/homebrew.slice.ts | 57 ++- src/stores/homebrew/homebrew.slice.type.ts | 13 +- .../homebrew/useListenToHomebrewContent.ts | 15 + src/types/HomebrewCollection.type.ts | 43 +- 28 files changed, 521 insertions(+), 301 deletions(-) create mode 100644 src/api-calls/homebrew/rules/listenToHomebrewRules.ts create mode 100644 src/api-calls/homebrew/rules/updateHomebrewRules.ts create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats.tsx create mode 100644 src/stores/homebrew/useListenToHomebrewContent.ts diff --git a/firestore.rules b/firestore.rules index e0327abe..93793eb2 100644 --- a/firestore.rules +++ b/firestore.rules @@ -106,6 +106,16 @@ service cloud.firestore { allow read: if request.auth.uid != null; allow create: if request.auth.uid != null; allow write: if request.auth.uid in resource.data.uids; + + function checkIsHomebrewOwner() { + let world = get(/databases/$(database)/documents/homebrew/$(homebrewId)).data; + return request.auth.uid in world.ownerIds; + } + + match /rules/rules { + allow read: if request.auth.uid != null; + allow write: if checkIsHomebrewOwner(); + } } match /users/{userId} { allow read: if true; diff --git a/package-lock.json b/package-lock.json index 2e198744..9fa9ef69 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ "@tiptap/starter-kit": "^2.0.0-beta.218", "dataforged": "^2.0.0-2", "dayjs": "^1.11.5", - "firebase": "^9.17.1", + "firebase": "^10.7.1", "formik": "^2.2.9", "immer": "^9.0.17", "material-ui-confirm": "^3.0.8", @@ -62,7 +62,7 @@ "eslint": "^8.54.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "typescript": "^4.9.3", + "typescript": "^5.3.3", "vite": "^4.0.0" } }, @@ -993,6 +993,14 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, "node_modules/@firebase/analytics": { "version": "0.10.0", "resolved": "https://registry.npmjs.org/@firebase/analytics/-/analytics-0.10.0.tgz", @@ -1029,9 +1037,9 @@ "integrity": "sha512-iRP+QKI2+oz3UAh4nPEq14CsEjrjD6a5+fuypjScisAh9kXKFvdJOZJDwk7kikLvWVLGEs9+kIUS4LPQV7VZVw==" }, "node_modules/@firebase/app": { - "version": "0.9.13", - "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.13.tgz", - "integrity": "sha512-GfiI1JxJ7ecluEmDjPzseRXk/PX31hS7+tjgBopL7XjB2hLUdR+0FTMXy2Q3/hXezypDvU6or7gVFizDESrkXw==", + "version": "0.9.25", + "resolved": "https://registry.npmjs.org/@firebase/app/-/app-0.9.25.tgz", + "integrity": "sha512-fX22gL5USXhOK21Hlh3oTeOzQZ6th6S2JrjXNEpBARmwzuUkqmVGVdsOCIFYIsLpK0dQE3o8xZnLrRg5wnzZ/g==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", @@ -1041,9 +1049,9 @@ } }, "node_modules/@firebase/app-check": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.0.tgz", - "integrity": "sha512-dRDnhkcaC2FspMiRK/Vbp+PfsOAEP6ZElGm9iGFJ9fDqHoPs0HOPn7dwpJ51lCFi1+2/7n5pRPGhqF/F03I97g==", + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@firebase/app-check/-/app-check-0.8.1.tgz", + "integrity": "sha512-zi3vbM5tb/eGRWyiqf+1DXbxFu9Q07dnm46rweodgUpH9B8svxYkHfNwYWx7F5mjHU70SQDuaojH1We5ws9OKA==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", @@ -1055,11 +1063,11 @@ } }, "node_modules/@firebase/app-check-compat": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.7.tgz", - "integrity": "sha512-cW682AxsyP1G+Z0/P7pO/WT2CzYlNxoNe5QejVarW2o5ZxeWSSPAiVEwpEpQR/bUlUmdeWThYTMvBWaopdBsqw==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@firebase/app-check-compat/-/app-check-compat-0.3.8.tgz", + "integrity": "sha512-EaETtChR4UgMokJFw+r6jfcIyCTUZSe0a6ivF37D9MxlG9G3wzK1COyXgxoX96GzXmDPc2aubX4PxCrdVHhrnA==", "dependencies": { - "@firebase/app-check": "0.8.0", + "@firebase/app-check": "0.8.1", "@firebase/app-check-types": "0.5.0", "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", @@ -1081,11 +1089,11 @@ "integrity": "sha512-uwSUj32Mlubybw7tedRzR24RP8M8JUVR3NPiMk3/Z4bCmgEKTlQBwMXrehDAZ2wF+TsBq0SN1c6ema71U/JPyQ==" }, "node_modules/@firebase/app-compat": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.13.tgz", - "integrity": "sha512-j6ANZaWjeVy5zg6X7uiqh6lM6o3n3LD1+/SJFNs9V781xyryyZWXe+tmnWNWPkP086QfJoNkWN9pMQRqSG4vMg==", + "version": "0.2.25", + "resolved": "https://registry.npmjs.org/@firebase/app-compat/-/app-compat-0.2.25.tgz", + "integrity": "sha512-B/JtCp1FsTuzlh1tIGQpYM2AXps21/zlzpFsk5LRsROOTRhBcR2N45AyaONPFD06C0yS0Tw19foxADzHyOSC3A==", "dependencies": { - "@firebase/app": "0.9.13", + "@firebase/app": "0.9.25", "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", "@firebase/util": "1.9.3", @@ -1103,31 +1111,37 @@ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" }, "node_modules/@firebase/auth": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-0.23.2.tgz", - "integrity": "sha512-dM9iJ0R6tI1JczuGSxXmQbXAgtYie0K4WvKcuyuSTCu9V8eEDiz4tfa1sO3txsfvwg7nOY3AjoCyMYEdqZ8hdg==", + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@firebase/auth/-/auth-1.5.1.tgz", + "integrity": "sha512-sVi7rq2YneLGJFqHa5S6nDfCHix9yuVV3RLhj/pWPlB4a36ofXal4E6PJwpeMc8uLjWEr1aovYN1jkXWNB6Avw==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", "@firebase/util": "1.9.3", - "node-fetch": "2.6.7", - "tslib": "^2.1.0" + "tslib": "^2.1.0", + "undici": "5.26.5" }, "peerDependencies": { - "@firebase/app": "0.x" + "@firebase/app": "0.x", + "@react-native-async-storage/async-storage": "^1.18.1" + }, + "peerDependenciesMeta": { + "@react-native-async-storage/async-storage": { + "optional": true + } } }, "node_modules/@firebase/auth-compat": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.4.2.tgz", - "integrity": "sha512-Q30e77DWXFmXEt5dg5JbqEDpjw9y3/PcP9LslDPR7fARmAOTIY9MM6HXzm9KC+dlrKH/+p6l8g9ifJiam9mc4A==", + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@firebase/auth-compat/-/auth-compat-0.5.1.tgz", + "integrity": "sha512-rgDZnrDoekRvtzXVji8Z61wxxkof6pTkjYEkybILrjM8tGP9tx4xa9qGpF4ax3AzF+rKr7mIa9NnoXEK4UNqmQ==", "dependencies": { - "@firebase/auth": "0.23.2", + "@firebase/auth": "1.5.1", "@firebase/auth-types": "0.12.0", "@firebase/component": "0.6.4", "@firebase/util": "1.9.3", - "node-fetch": "2.6.7", - "tslib": "^2.1.0" + "tslib": "^2.1.0", + "undici": "5.26.5" }, "peerDependencies": { "@firebase/app-compat": "0.x" @@ -1157,10 +1171,11 @@ } }, "node_modules/@firebase/database": { - "version": "0.14.4", - "resolved": "https://registry.npmjs.org/@firebase/database/-/database-0.14.4.tgz", - "integrity": "sha512-+Ea/IKGwh42jwdjCyzTmeZeLM3oy1h0mFPsTy6OqCWzcu/KFqRAr5Tt1HRCOBlNOdbh84JPZC47WLU18n2VbxQ==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.2.tgz", + "integrity": "sha512-8X6NBJgUQzDz0xQVaCISoOLINKat594N2eBbMR3Mu/MH/ei4WM+aAMlsNzngF22eljXu1SILP5G3evkyvsG3Ng==", "dependencies": { + "@firebase/app-check-interop-types": "0.3.0", "@firebase/auth-interop-types": "0.2.1", "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", @@ -1170,40 +1185,40 @@ } }, "node_modules/@firebase/database-compat": { - "version": "0.3.4", - "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-0.3.4.tgz", - "integrity": "sha512-kuAW+l+sLMUKBThnvxvUZ+Q1ZrF/vFJ58iUY9kAcbX48U03nVzIF6Tmkf0p3WVQwMqiXguSgtOPIB6ZCeF+5Gg==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-1.0.2.tgz", + "integrity": "sha512-09ryJnXDvuycsxn8aXBzLhBTuCos3HEnCOBWY6hosxfYlNCGnLvG8YMlbSAt5eNhf7/00B095AEfDsdrrLjxqA==", "dependencies": { "@firebase/component": "0.6.4", - "@firebase/database": "0.14.4", - "@firebase/database-types": "0.10.4", + "@firebase/database": "1.0.2", + "@firebase/database-types": "1.0.0", "@firebase/logger": "0.4.0", "@firebase/util": "1.9.3", "tslib": "^2.1.0" } }, "node_modules/@firebase/database-types": { - "version": "0.10.4", - "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-0.10.4.tgz", - "integrity": "sha512-dPySn0vJ/89ZeBac70T+2tWWPiJXWbmRygYv0smT5TfE3hDrQ09eKMF3Y+vMlTdrMWq7mUdYW5REWPSGH4kAZQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.0.tgz", + "integrity": "sha512-SjnXStoE0Q56HcFgNQ+9SsmJc0c8TqGARdI/T44KXy+Ets3r6x/ivhQozT66bMnCEjJRywYoxNurRTMlZF8VNg==", "dependencies": { "@firebase/app-types": "0.9.0", "@firebase/util": "1.9.3" } }, "node_modules/@firebase/firestore": { - "version": "3.13.0", - "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-3.13.0.tgz", - "integrity": "sha512-NwcnU+madJXQ4fbLkGx1bWvL612IJN/qO6bZ6dlPmyf7QRyu5azUosijdAN675r+bOOJxMtP1Bv981bHBXAbUg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore/-/firestore-4.4.0.tgz", + "integrity": "sha512-VeDXD9PUjvcWY1tInBOMTIu2pijR3YYy+QAe5cxCo1Q1vW+aA/mpQHhebPM1J6b4Zd1MuUh8xpBRvH9ujKR56A==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/logger": "0.4.0", "@firebase/util": "1.9.3", - "@firebase/webchannel-wrapper": "0.10.1", - "@grpc/grpc-js": "~1.7.0", - "@grpc/proto-loader": "^0.6.13", - "node-fetch": "2.6.7", - "tslib": "^2.1.0" + "@firebase/webchannel-wrapper": "0.10.5", + "@grpc/grpc-js": "~1.9.0", + "@grpc/proto-loader": "^0.7.8", + "tslib": "^2.1.0", + "undici": "5.26.5" }, "engines": { "node": ">=10.10.0" @@ -1213,13 +1228,13 @@ } }, "node_modules/@firebase/firestore-compat": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.12.tgz", - "integrity": "sha512-mazuNGAx5Kt9Nph0pm6ULJFp/+j7GSsx+Ncw1GrnKl+ft1CQ4q2LcUssXnjqkX2Ry0fNGqUzC1mfIUrk9bYtjQ==", + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/@firebase/firestore-compat/-/firestore-compat-0.3.23.tgz", + "integrity": "sha512-uUTBiP0GLVBETaOCfB11d33OWB8x1r2G1Xrl0sRK3Va0N5LJ/GRvKVSGfM7VScj+ypeHe8RpdwKoCqLpN1e+uA==", "dependencies": { "@firebase/component": "0.6.4", - "@firebase/firestore": "3.13.0", - "@firebase/firestore-types": "2.5.1", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-types": "3.0.0", "@firebase/util": "1.9.3", "tslib": "^2.1.0" }, @@ -1228,38 +1243,38 @@ } }, "node_modules/@firebase/firestore-types": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-2.5.1.tgz", - "integrity": "sha512-xG0CA6EMfYo8YeUxC8FeDzf6W3FX1cLlcAGBYV6Cku12sZRI81oWcu61RSKM66K6kUENP+78Qm8mvroBcm1whw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@firebase/firestore-types/-/firestore-types-3.0.0.tgz", + "integrity": "sha512-Meg4cIezHo9zLamw0ymFYBD4SMjLb+ZXIbuN7T7ddXN6MGoICmOTq3/ltdCGoDCS2u+H1XJs2u/cYp75jsX9Qw==", "peerDependencies": { "@firebase/app-types": "0.x", "@firebase/util": "1.x" } }, "node_modules/@firebase/functions": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.10.0.tgz", - "integrity": "sha512-2U+fMNxTYhtwSpkkR6WbBcuNMOVaI7MaH3cZ6UAeNfj7AgEwHwMIFLPpC13YNZhno219F0lfxzTAA0N62ndWzA==", + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@firebase/functions/-/functions-0.11.0.tgz", + "integrity": "sha512-n1PZxKnJ++k73Q8khTPwihlbeKo6emnGzE0hX6QVQJsMq82y/XKmNpw2t/q30VJgwaia3ZXU1fd1C5wHncL+Zg==", "dependencies": { "@firebase/app-check-interop-types": "0.3.0", "@firebase/auth-interop-types": "0.2.1", "@firebase/component": "0.6.4", "@firebase/messaging-interop-types": "0.2.0", "@firebase/util": "1.9.3", - "node-fetch": "2.6.7", - "tslib": "^2.1.0" + "tslib": "^2.1.0", + "undici": "5.26.5" }, "peerDependencies": { "@firebase/app": "0.x" } }, "node_modules/@firebase/functions-compat": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.5.tgz", - "integrity": "sha512-uD4jwgwVqdWf6uc3NRKF8cSZ0JwGqSlyhPgackyUPe+GAtnERpS4+Vr66g0b3Gge0ezG4iyHo/EXW/Hjx7QhHw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@firebase/functions-compat/-/functions-compat-0.3.6.tgz", + "integrity": "sha512-RQpO3yuHtnkqLqExuAT2d0u3zh8SDbeBYK5EwSCBKI9mjrFeJRXBnd3pEG+x5SxGJLy56/5pQf73mwt0OuH5yg==", "dependencies": { "@firebase/component": "0.6.4", - "@firebase/functions": "0.10.0", + "@firebase/functions": "0.11.0", "@firebase/functions-types": "0.6.0", "@firebase/util": "1.9.3", "tslib": "^2.1.0" @@ -1319,15 +1334,15 @@ } }, "node_modules/@firebase/messaging": { - "version": "0.12.4", - "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.4.tgz", - "integrity": "sha512-6JLZct6zUaex4g7HI3QbzeUrg9xcnmDAPTWpkoMpd/GoSVWH98zDoWXMGrcvHeCAIsLpFMe4MPoZkJbrPhaASw==", + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging/-/messaging-0.12.5.tgz", + "integrity": "sha512-i/rrEI2k9ueFhdIr8KQsptWGskrsnkC5TkohCTrJKz9P0C/PbNv14IAMkwhMJTqIur5VwuOnrUkc9Kdz7awekw==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/installations": "0.6.4", "@firebase/messaging-interop-types": "0.2.0", "@firebase/util": "1.9.3", - "idb": "7.0.1", + "idb": "7.1.1", "tslib": "^2.1.0" }, "peerDependencies": { @@ -1335,12 +1350,12 @@ } }, "node_modules/@firebase/messaging-compat": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.4.tgz", - "integrity": "sha512-lyFjeUhIsPRYDPNIkYX1LcZMpoVbBWXX4rPl7c/rqc7G+EUea7IEtSt4MxTvh6fDfPuzLn7+FZADfscC+tNMfg==", + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@firebase/messaging-compat/-/messaging-compat-0.2.5.tgz", + "integrity": "sha512-qHQZxm4hEG8/HFU/ls5/bU+rpnlPDoZoqi3ATMeb6s4hovYV9+PfV5I7ZrKV5eFFv47Hx1PWLe5uPnS4e7gMwQ==", "dependencies": { "@firebase/component": "0.6.4", - "@firebase/messaging": "0.12.4", + "@firebase/messaging": "0.12.5", "@firebase/util": "1.9.3", "tslib": "^2.1.0" }, @@ -1353,6 +1368,11 @@ "resolved": "https://registry.npmjs.org/@firebase/messaging-interop-types/-/messaging-interop-types-0.2.0.tgz", "integrity": "sha512-ujA8dcRuVeBixGR9CtegfpU4YmZf3Lt7QYkcj693FFannwNuZgfAYaTmbJ40dtjB81SAu6tbFPL9YLNT15KmOQ==" }, + "node_modules/@firebase/messaging/node_modules/idb": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz", + "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==" + }, "node_modules/@firebase/performance": { "version": "0.6.4", "resolved": "https://registry.npmjs.org/@firebase/performance/-/performance-0.6.4.tgz", @@ -1426,26 +1446,26 @@ "integrity": "sha512-RtEH4vdcbXZuZWRZbIRmQVBNsE7VDQpet2qFvq6vwKLBIQRQR5Kh58M4ok3A3US8Sr3rubYnaGqZSurCwI8uMA==" }, "node_modules/@firebase/storage": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.11.2.tgz", - "integrity": "sha512-CtvoFaBI4hGXlXbaCHf8humajkbXhs39Nbh6MbNxtwJiCqxPy9iH3D3CCfXAvP0QvAAwmJUTK3+z9a++Kc4nkA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@firebase/storage/-/storage-0.12.0.tgz", + "integrity": "sha512-SGs02Y/mmWBRsqZiYLpv4Sf7uZYZzMWVNN+aKiDqPsFBCzD6hLvGkXz+u98KAl8FqcjgB8BtSu01wm4pm76KHA==", "dependencies": { "@firebase/component": "0.6.4", "@firebase/util": "1.9.3", - "node-fetch": "2.6.7", - "tslib": "^2.1.0" + "tslib": "^2.1.0", + "undici": "5.26.5" }, "peerDependencies": { "@firebase/app": "0.x" } }, "node_modules/@firebase/storage-compat": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.2.tgz", - "integrity": "sha512-wvsXlLa9DVOMQJckbDNhXKKxRNNewyUhhbXev3t8kSgoCotd1v3MmqhKKz93ePhDnhHnDs7bYHy+Qa8dRY6BXw==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/storage-compat/-/storage-compat-0.3.3.tgz", + "integrity": "sha512-WNtjYPhpOA1nKcRu5lIodX0wZtP8pI0VxDJnk6lr+av7QZNS1s6zvr+ERDTve+Qu4Hq/ZnNaf3kBEQR2ccXn6A==", "dependencies": { "@firebase/component": "0.6.4", - "@firebase/storage": "0.11.2", + "@firebase/storage": "0.12.0", "@firebase/storage-types": "0.8.0", "@firebase/util": "1.9.3", "tslib": "^2.1.0" @@ -1472,9 +1492,9 @@ } }, "node_modules/@firebase/webchannel-wrapper": { - "version": "0.10.1", - "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.1.tgz", - "integrity": "sha512-Dq5rYfEpdeel0bLVN+nfD1VWmzCkK+pJbSjIawGE+RY4+NIJqhbUDDQjvV0NUK84fMfwxvtFoCtEe70HfZjFcw==" + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-0.10.5.tgz", + "integrity": "sha512-eSkJsnhBWv5kCTSU1tSUVl9mpFu+5NXXunZc83le8GMjMlsWwQArSc7cJJ4yl+aDFY0NGLi0AjZWMn1axOrkRg==" }, "node_modules/@floating-ui/core": { "version": "1.5.0", @@ -1526,18 +1546,18 @@ "integrity": "sha512-zphSXq1q4mvLwMG6X5K/AG5BLcldDQbT4aZYMZpO/+FnGlTH2hTRII3axeozNDRlRX0UuSetamV7ccq3mBmTUQ==" }, "node_modules/@grpc/grpc-js": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.7.3.tgz", - "integrity": "sha512-H9l79u4kJ2PVSxUNA08HMYAnUBLj9v6KjYQ7SQ71hOZcEXhShE/y5iQCesP8+6/Ik/7i2O0a10bPquIcYfufog==", + "version": "1.9.13", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.13.tgz", + "integrity": "sha512-OEZZu9v9AA+7/tghMDE8o5DAMD5THVnwSqDWuh7PPYO5287rTyqy0xEHT6/e4pbqSrhyLPdQFsam4TwFQVVIIw==", "dependencies": { - "@grpc/proto-loader": "^0.7.0", + "@grpc/proto-loader": "^0.7.8", "@types/node": ">=12.12.47" }, "engines": { "node": "^8.13.0 || >=10.10.0" } }, - "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "node_modules/@grpc/proto-loader": { "version": "0.7.10", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.10.tgz", "integrity": "sha512-CAqDfoaQ8ykFd9zqBDn4k6iWT9loLAlc2ETmDFS9JCD70gDcnA4L3AFEo2iV7KyAtAAHFW9ftq1Fz+Vsgq80RQ==", @@ -1554,89 +1574,6 @@ "node": ">=6" } }, - "node_modules/@grpc/proto-loader": { - "version": "0.6.13", - "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.6.13.tgz", - "integrity": "sha512-FjxPYDRTn6Ec3V0arm1FtSpmP6V50wuph2yILpyvTKzjc76oDdoihXqM1DzOW5ubvCC8GivfCnNtfaRE8myJ7g==", - "dependencies": { - "@types/long": "^4.0.1", - "lodash.camelcase": "^4.3.0", - "long": "^4.0.0", - "protobufjs": "^6.11.3", - "yargs": "^16.2.0" - }, - "bin": { - "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@grpc/proto-loader/node_modules/cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "node_modules/@grpc/proto-loader/node_modules/long": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz", - "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA==" - }, - "node_modules/@grpc/proto-loader/node_modules/protobufjs": { - "version": "6.11.4", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-6.11.4.tgz", - "integrity": "sha512-5kQWPaJHi1WoCpjTGszzQ32PG2F4+wRY6BmAT4Vfw56Q2FZ4YZzK20xUYQH4YkfehY1e6QSICrJquM6xXZNcrw==", - "hasInstallScript": true, - "dependencies": { - "@protobufjs/aspromise": "^1.1.2", - "@protobufjs/base64": "^1.1.2", - "@protobufjs/codegen": "^2.0.4", - "@protobufjs/eventemitter": "^1.1.0", - "@protobufjs/fetch": "^1.1.0", - "@protobufjs/float": "^1.0.2", - "@protobufjs/inquire": "^1.1.0", - "@protobufjs/path": "^1.1.2", - "@protobufjs/pool": "^1.1.0", - "@protobufjs/utf8": "^1.1.0", - "@types/long": "^4.0.1", - "@types/node": ">=13.7.0", - "long": "^4.0.0" - }, - "bin": { - "pbjs": "bin/pbjs", - "pbts": "bin/pbts" - } - }, - "node_modules/@grpc/proto-loader/node_modules/yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "dependencies": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@grpc/proto-loader/node_modules/yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "engines": { - "node": ">=10" - } - }, "node_modules/@hocuspocus/transformer": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/@hocuspocus/transformer/-/transformer-2.7.1.tgz", @@ -2908,11 +2845,6 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, - "node_modules/@types/long": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", - "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==" - }, "node_modules/@types/mdast": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz", @@ -4689,35 +4621,35 @@ } }, "node_modules/firebase": { - "version": "9.23.0", - "resolved": "https://registry.npmjs.org/firebase/-/firebase-9.23.0.tgz", - "integrity": "sha512-/4lUVY0lUvBDIaeY1q6dUYhS8Sd18Qb9CgWkPZICUo9IXpJNCEagfNZXBBFCkMTTN5L5gx2Hjr27y21a9NzUcA==", + "version": "10.7.1", + "resolved": "https://registry.npmjs.org/firebase/-/firebase-10.7.1.tgz", + "integrity": "sha512-Mlt7y7zQ43FtKp4SCyYie3tnrOL3UMF2XXiV4ZXMrC0d0wtcOYmABuybhkJpJCKILpdekxr39wjnaai0DZlWFg==", "dependencies": { "@firebase/analytics": "0.10.0", "@firebase/analytics-compat": "0.2.6", - "@firebase/app": "0.9.13", - "@firebase/app-check": "0.8.0", - "@firebase/app-check-compat": "0.3.7", - "@firebase/app-compat": "0.2.13", + "@firebase/app": "0.9.25", + "@firebase/app-check": "0.8.1", + "@firebase/app-check-compat": "0.3.8", + "@firebase/app-compat": "0.2.25", "@firebase/app-types": "0.9.0", - "@firebase/auth": "0.23.2", - "@firebase/auth-compat": "0.4.2", - "@firebase/database": "0.14.4", - "@firebase/database-compat": "0.3.4", - "@firebase/firestore": "3.13.0", - "@firebase/firestore-compat": "0.3.12", - "@firebase/functions": "0.10.0", - "@firebase/functions-compat": "0.3.5", + "@firebase/auth": "1.5.1", + "@firebase/auth-compat": "0.5.1", + "@firebase/database": "1.0.2", + "@firebase/database-compat": "1.0.2", + "@firebase/firestore": "4.4.0", + "@firebase/firestore-compat": "0.3.23", + "@firebase/functions": "0.11.0", + "@firebase/functions-compat": "0.3.6", "@firebase/installations": "0.6.4", "@firebase/installations-compat": "0.2.4", - "@firebase/messaging": "0.12.4", - "@firebase/messaging-compat": "0.2.4", + "@firebase/messaging": "0.12.5", + "@firebase/messaging-compat": "0.2.5", "@firebase/performance": "0.6.4", "@firebase/performance-compat": "0.2.4", "@firebase/remote-config": "0.4.4", "@firebase/remote-config-compat": "0.2.4", - "@firebase/storage": "0.11.2", - "@firebase/storage-compat": "0.3.2", + "@firebase/storage": "0.12.0", + "@firebase/storage-compat": "0.3.3", "@firebase/util": "1.9.3" } }, @@ -6719,25 +6651,6 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, - "node_modules/node-fetch": { - "version": "2.6.7", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", - "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", - "dependencies": { - "whatwg-url": "^5.0.0" - }, - "engines": { - "node": "4.x || >=6.0.0" - }, - "peerDependencies": { - "encoding": "^0.1.0" - }, - "peerDependenciesMeta": { - "encoding": { - "optional": true - } - } - }, "node_modules/node-releases": { "version": "2.0.13", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz", @@ -8196,11 +8109,6 @@ "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" }, - "node_modules/tr46": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", - "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" - }, "node_modules/trim-lines": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", @@ -8344,16 +8252,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.9.5.tgz", - "integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", + "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", "devOptional": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uc.micro": { @@ -8376,6 +8284,17 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/undici": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", + "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -8664,11 +8583,6 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, - "node_modules/webidl-conversions": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", - "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -8690,15 +8604,6 @@ "node": ">=0.8.0" } }, - "node_modules/whatwg-url": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", - "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dependencies": { - "tr46": "~0.0.3", - "webidl-conversions": "^3.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", diff --git a/package.json b/package.json index 599e860c..dc89545e 100644 --- a/package.json +++ b/package.json @@ -29,7 +29,7 @@ "@tiptap/starter-kit": "^2.0.0-beta.218", "dataforged": "^2.0.0-2", "dayjs": "^1.11.5", - "firebase": "^9.17.1", + "firebase": "^10.7.1", "formik": "^2.2.9", "immer": "^9.0.17", "material-ui-confirm": "^3.0.8", @@ -64,7 +64,7 @@ "eslint": "^8.54.0", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", - "typescript": "^4.9.3", + "typescript": "^5.3.3", "vite": "^4.0.0" } } diff --git a/src/api-calls/assets/updateAssetCheckbox.ts b/src/api-calls/assets/updateAssetCheckbox.ts index 6eac411b..8a599d0c 100644 --- a/src/api-calls/assets/updateAssetCheckbox.ts +++ b/src/api-calls/assets/updateAssetCheckbox.ts @@ -23,7 +23,6 @@ export const updateAssetCheckbox = createApiFunction< characterId ? getCharacterAssetDoc(characterId, assetId) : getCampaignAssetDoc(campaignId as string, assetId), - //@ts-expect-error - typescript doesn't like this notation { [`enabledAbilities.${abilityIndex}`]: checked, } diff --git a/src/api-calls/assets/updateAssetCondition.ts b/src/api-calls/assets/updateAssetCondition.ts index 629e0dd2..0525a2df 100644 --- a/src/api-calls/assets/updateAssetCondition.ts +++ b/src/api-calls/assets/updateAssetCondition.ts @@ -22,7 +22,6 @@ export const updateAssetCondition = createApiFunction< characterId ? getCharacterAssetDoc(characterId, assetId) : getCampaignAssetDoc(campaignId as string, assetId), - // @ts-expect-error - typescript doesn't like this notation { [`conditions.${condition}`]: checked, } diff --git a/src/api-calls/assets/updateAssetInput.ts b/src/api-calls/assets/updateAssetInput.ts index 5684d0a0..980c364d 100644 --- a/src/api-calls/assets/updateAssetInput.ts +++ b/src/api-calls/assets/updateAssetInput.ts @@ -23,7 +23,6 @@ export const updateAssetInput = createApiFunction< characterId ? getCharacterAssetDoc(characterId, assetId) : getCampaignAssetDoc(campaignId as string, assetId), - // @ts-expect-error Typescript doesn't like this notation, but it works { [`inputs.${inputLabel}`]: inputValue, } diff --git a/src/api-calls/game-log/updateLog.ts b/src/api-calls/game-log/updateLog.ts index e6d3e0a5..f6d2a048 100644 --- a/src/api-calls/game-log/updateLog.ts +++ b/src/api-calls/game-log/updateLog.ts @@ -17,12 +17,12 @@ export const updateLog = createApiFunction< reject(new Error("Either campaign or character ID must be defined.")); } - updateDoc( - campaignId - ? getCampaignGameLogDocument(campaignId, logId) - : getCharacterGameLogDocument(characterId as string, logId), - log - ) + const docRef = campaignId + ? getCampaignGameLogDocument(campaignId, logId) + : getCharacterGameLogDocument(characterId as string, logId); + + // @ts-expect-error not sure why log is incorrect here + updateDoc(docRef, log) .then(() => { resolve(); }) diff --git a/src/api-calls/homebrew/rules/_getRef.ts b/src/api-calls/homebrew/rules/_getRef.ts index 9cd8984a..0e305a58 100644 --- a/src/api-calls/homebrew/rules/_getRef.ts +++ b/src/api-calls/homebrew/rules/_getRef.ts @@ -2,7 +2,6 @@ import { DocumentReference, doc } from "firebase/firestore"; import { constructHomebrewCollectionDocPath } from "../_getRef"; import { firestore } from "config/firebase.config"; import { StoredRules } from "types/HomebrewCollection.type"; -import { Rules } from "@datasworn/core"; export function constructHomebrewRulesDocPath(homebrewId: string) { return `${constructHomebrewCollectionDocPath(homebrewId)}/rules/rules`; @@ -12,13 +11,5 @@ export function getHomebrewRulesDoc(homebrewId: string) { return doc( firestore, constructHomebrewRulesDocPath(homebrewId) - ) as DocumentReference>; + ) as DocumentReference; } - -export type ReplaceRecord = { - [P in keyof T]: T[P] extends Record - ? { [key in A]: B } - : T[P] extends object - ? ReplaceRecord - : T[P]; -}; diff --git a/src/api-calls/homebrew/rules/createHomebrewRules.ts b/src/api-calls/homebrew/rules/createHomebrewRules.ts index b69e66ef..001e3ba9 100644 --- a/src/api-calls/homebrew/rules/createHomebrewRules.ts +++ b/src/api-calls/homebrew/rules/createHomebrewRules.ts @@ -1,14 +1,21 @@ -import { Rules } from "@datasworn/core"; import { createApiFunction } from "api-calls/createApiFunction"; +import { PartialWithFieldValue, updateDoc } from "firebase/firestore"; +import { getHomebrewRulesDoc } from "./_getRef"; +import { StoredRules } from "types/HomebrewCollection.type"; export const updateHomebrewRules = createApiFunction< { homebrewId: string; - rules: Rules; + rules: PartialWithFieldValue; }, void ->(() => { +>((params) => { + const { homebrewId, rules } = params; return new Promise((resolve, reject) => { - resolve(); + updateDoc(getHomebrewRulesDoc(homebrewId), rules) + .then(() => { + resolve(); + }) + .catch(reject); }); }, "Failed to update rules."); diff --git a/src/api-calls/homebrew/rules/listenToHomebrewRules.ts b/src/api-calls/homebrew/rules/listenToHomebrewRules.ts new file mode 100644 index 00000000..8e3b980f --- /dev/null +++ b/src/api-calls/homebrew/rules/listenToHomebrewRules.ts @@ -0,0 +1,26 @@ +import { onSnapshot } from "firebase/firestore"; +import { StoredRules } from "types/HomebrewCollection.type"; +import { getHomebrewRulesDoc } from "./_getRef"; + +export function listenToHomebrewRules( + homebrewId: string, + updateRules: (collectionId: string, rules: StoredRules) => void, + onError: (error: unknown) => void, + onLoaded: () => void +) { + return onSnapshot( + getHomebrewRulesDoc(homebrewId), + (snapshot) => { + const doc = snapshot.data(); + if (doc) { + updateRules(homebrewId, doc); + } else { + onLoaded(); + } + }, + (error) => { + console.error(error); + onError(error); + } + ); +} diff --git a/src/api-calls/homebrew/rules/updateHomebrewRules.ts b/src/api-calls/homebrew/rules/updateHomebrewRules.ts new file mode 100644 index 00000000..001e3ba9 --- /dev/null +++ b/src/api-calls/homebrew/rules/updateHomebrewRules.ts @@ -0,0 +1,21 @@ +import { createApiFunction } from "api-calls/createApiFunction"; +import { PartialWithFieldValue, updateDoc } from "firebase/firestore"; +import { getHomebrewRulesDoc } from "./_getRef"; +import { StoredRules } from "types/HomebrewCollection.type"; + +export const updateHomebrewRules = createApiFunction< + { + homebrewId: string; + rules: PartialWithFieldValue; + }, + void +>((params) => { + const { homebrewId, rules } = params; + return new Promise((resolve, reject) => { + updateDoc(getHomebrewRulesDoc(homebrewId), rules) + .then(() => { + resolve(); + }) + .catch(reject); + }); +}, "Failed to update rules."); diff --git a/src/components/shared/Layout/nav/NavRailFlyouts/CampaignMenu.tsx b/src/components/shared/Layout/nav/NavRailFlyouts/CampaignMenu.tsx index 2aa3e724..3aa1839d 100644 --- a/src/components/shared/Layout/nav/NavRailFlyouts/CampaignMenu.tsx +++ b/src/components/shared/Layout/nav/NavRailFlyouts/CampaignMenu.tsx @@ -1,4 +1,5 @@ import { + Box, Collapse, List, ListItem, @@ -38,7 +39,7 @@ export function CampaignMenu() { {Object.keys(campaigns).map((campaignId) => ( - <> + {campaigns[campaignId].gmIds?.includes(uid) ? ( <> @@ -106,7 +107,7 @@ export function CampaignMenu() { )} - + ))} diff --git a/src/components/shared/Layout/nav/NavRailFlyouts/HomebrewMenu.tsx b/src/components/shared/Layout/nav/NavRailFlyouts/HomebrewMenu.tsx index 27a5862b..a4708119 100644 --- a/src/components/shared/Layout/nav/NavRailFlyouts/HomebrewMenu.tsx +++ b/src/components/shared/Layout/nav/NavRailFlyouts/HomebrewMenu.tsx @@ -25,7 +25,7 @@ export function HomebrewMenu() { > diff --git a/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx b/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx index bd198979..8bd79ea7 100644 --- a/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx +++ b/src/pages/Campaign/CampaignGMScreenPage/CampaignGMScreenPage.tsx @@ -1,10 +1,11 @@ -import { LinearProgress } from "@mui/material"; +import { Button, LinearProgress } from "@mui/material"; import { useSnackbar } from "providers/SnackbarProvider/useSnackbar"; import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; import { TabsSection } from "./components/TabsSection"; import { CAMPAIGN_ROUTES, + constructCampaignPath, constructCampaignSheetPath, } from "pages/Campaign/routes"; import { PageContent, PageHeader } from "components/shared/Layout"; @@ -14,6 +15,7 @@ import { useStore } from "stores/store"; import { Sidebar } from "pages/Character/CharacterSheetPage/components/Sidebar"; import { SectionWithSidebar } from "components/shared/Layout/SectionWithSidebar"; import { EmptyState } from "components/shared/EmptyState"; +import { LinkComponent } from "components/shared/LinkComponent"; export function CampaignGMScreenPage() { useSyncStore(); @@ -61,11 +63,28 @@ export function CampaignGMScreenPage() { return ; } - if (!campaign || !uid || !campaign?.gmIds?.includes(uid)) { + if (!campaign || !campaignId) { return ( - + + Select a Campaign + + } + /> ); } + if (!uid || !campaign.gmIds?.includes(uid)) { + return null; + } return ( <> diff --git a/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx b/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx index 27703491..15725a7a 100644 --- a/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx +++ b/src/pages/Campaign/CampaignSheetPage/CampaignSheetPage.tsx @@ -1,4 +1,4 @@ -import { LinearProgress } from "@mui/material"; +import { Button, LinearProgress } from "@mui/material"; import { useEffect, useState } from "react"; import { useSearchParams } from "react-router-dom"; import { CampaignSheetHeader } from "./components/CampaignSheetHeader"; @@ -15,6 +15,9 @@ import { useSyncStore } from "./hooks/useSyncStore"; import { ClockSection } from "components/features/charactersAndCampaigns/Clocks/ClockSection"; import { useGameSystem } from "hooks/useGameSystem"; import { GAME_SYSTEMS } from "types/GameSystems.type"; +import { EmptyState } from "components/shared/EmptyState"; +import { LinkComponent } from "components/shared/LinkComponent"; +import { CAMPAIGN_ROUTES, constructCampaignPath } from "../routes"; enum TABS { CHARACTER = "characters", @@ -63,7 +66,23 @@ export function CampaignSheetPage() { } if (!campaignId || !campaign) { - return null; + return ( + + Select a Campaign + + } + /> + ); } return ( diff --git a/src/pages/Character/CharacterSheetPage/CharacterSheetPage.tsx b/src/pages/Character/CharacterSheetPage/CharacterSheetPage.tsx index 579ead89..e1fd98a7 100644 --- a/src/pages/Character/CharacterSheetPage/CharacterSheetPage.tsx +++ b/src/pages/Character/CharacterSheetPage/CharacterSheetPage.tsx @@ -1,10 +1,9 @@ import { Button, LinearProgress } from "@mui/material"; -import { Link } from "react-router-dom"; import { EmptyState } from "components/shared/EmptyState/EmptyState"; import { TabsSection } from "./components/TabsSection"; import { TracksSection } from "./components/TracksSection"; import { CharacterHeader } from "./components/CharacterHeader"; -import { CHARACTER_ROUTES, characterPaths } from "../routes"; +import { CHARACTER_ROUTES, constructCharacterPath } from "../routes"; import { PageContent, PageHeader } from "components/shared/Layout"; import { Head } from "providers/HeadProvider/Head"; import { useStore } from "stores/store"; @@ -14,6 +13,7 @@ import { Sidebar } from "./components/Sidebar"; import { SectionWithSidebar } from "components/shared/Layout/SectionWithSidebar"; import { useIsMobile } from "hooks/useIsMobile"; import { StatsSectionMobile } from "./components/StatsSectionMobile"; +import { LinkComponent } from "components/shared/LinkComponent"; export function CharacterSheetPage() { useSyncStore(); @@ -51,8 +51,8 @@ export function CharacterSheetPage() { showImage callToAction={ + setStatDialogOpen(false)}> + setStatDialogOpen(false)}> + {editingStatKey ? "Edit" : "Add"} Stat + + + + setEditingStatLabel(evt.currentTarget.value)} + /> + + setEditingStatDescription(evt.currentTarget.value) + } + /> + + Example + + + + + + + + + + + ); +} diff --git a/src/pages/Homebrew/HomebrewSelectPage/HomebrewSelectPage.tsx b/src/pages/Homebrew/HomebrewSelectPage/HomebrewSelectPage.tsx index 144e5fd0..2b907aef 100644 --- a/src/pages/Homebrew/HomebrewSelectPage/HomebrewSelectPage.tsx +++ b/src/pages/Homebrew/HomebrewSelectPage/HomebrewSelectPage.tsx @@ -34,13 +34,13 @@ export function HomebrewSelectPage() { } const collectionKeys = Object.keys(homebrewCollections).sort((k1, k2) => - (homebrewCollections[k1].title ?? "Unnamed Collection")?.localeCompare( - homebrewCollections[k2].title ?? "Unnamed Collection" + (homebrewCollections[k1].base.title ?? "Unnamed Collection")?.localeCompare( + homebrewCollections[k2].base.title ?? "Unnamed Collection" ) ); const collectionIds = Object.values(homebrewCollections).map( - (collection) => collection.id + (collection) => collection.base.id ); return ( @@ -125,7 +125,7 @@ export function HomebrewSelectPage() { > - {homebrewCollections[collectionKey].title ?? + {homebrewCollections[collectionKey].base.title ?? "Unnamed Collection"} diff --git a/src/pages/World/WorldSelectPage/components/WorldCard.tsx b/src/pages/World/WorldSelectPage/components/WorldCard.tsx index aef4b899..052ae9a5 100644 --- a/src/pages/World/WorldSelectPage/components/WorldCard.tsx +++ b/src/pages/World/WorldSelectPage/components/WorldCard.tsx @@ -32,7 +32,7 @@ export function WorldCard(props: WorldCardProps) { }, [ownerIds, loadUsers]); return ( - + + Select a World + + } + /> + ); } const handleDeleteClick = () => { diff --git a/src/stores/homebrew/homebrew.slice.ts b/src/stores/homebrew/homebrew.slice.ts index 091644fa..f3c99177 100644 --- a/src/stores/homebrew/homebrew.slice.ts +++ b/src/stores/homebrew/homebrew.slice.ts @@ -6,6 +6,8 @@ import { getErrorMessage } from "functions/getErrorMessage"; import { createHomebrewExpansion } from "api-calls/homebrew/createHomebrewExpansion"; import { updateHomebrewExpansion } from "api-calls/homebrew/updateHomebrewExpansion"; import { deleteHomebrewExpansion } from "api-calls/homebrew/deleteHomebrewExpansion"; +import { Unsubscribe } from "firebase/firestore"; +import { listenToHomebrewRules } from "api-calls/homebrew/rules/listenToHomebrewRules"; export const createHomebrewSlice: CreateSliceType = (set) => ({ ...defaultHomebrewSlice, @@ -14,7 +16,10 @@ export const createHomebrewSlice: CreateSliceType = (set) => ({ uid, (collectionId, collection) => { set((store) => { - store.homebrew.collections[collectionId] = collection; + store.homebrew.collections[collectionId] = { + ...(store.homebrew.collections[collectionId] ?? {}), + base: collection, + }; store.homebrew.loading = false; store.homebrew.error = undefined; }); @@ -42,6 +47,56 @@ export const createHomebrewSlice: CreateSliceType = (set) => ({ } ); }, + + subscribeToHomebrewContent: (homebrewIds) => { + const unsubscribes: Unsubscribe[] = []; + homebrewIds.forEach((homebrewId) => { + unsubscribes.push( + listenToHomebrewRules( + homebrewId, + (id, rules) => { + set((store) => { + store.homebrew.collections[homebrewId] = { + ...(store.homebrew.collections[homebrewId] ?? {}), + rules: { + data: rules, + loaded: true, + }, + }; + }); + }, + (error) => { + set((store) => { + store.homebrew.collections[homebrewId] = { + ...(store.homebrew.collections[homebrewId] ?? {}), + rules: { + ...(store.homebrew.collections[homebrewId].rules ?? {}), + loaded: true, + error: getErrorMessage(error, "Failed to load rules"), + }, + }; + }); + }, + () => { + set((store) => { + store.homebrew.collections[homebrewId] = { + ...(store.homebrew.collections[homebrewId] ?? {}), + rules: { + ...(store.homebrew.collections[homebrewId].rules ?? {}), + loaded: true, + }, + }; + }); + } + ) + ); + }); + + return () => { + unsubscribes.forEach((unsubscribe) => unsubscribe()); + }; + }, + createExpansion: (expansion) => { return createHomebrewExpansion(expansion); }, diff --git a/src/stores/homebrew/homebrew.slice.type.ts b/src/stores/homebrew/homebrew.slice.type.ts index e91f923c..108f2d3a 100644 --- a/src/stores/homebrew/homebrew.slice.type.ts +++ b/src/stores/homebrew/homebrew.slice.type.ts @@ -2,16 +2,27 @@ import { Unsubscribe } from "firebase/firestore"; import { BaseExpansion, BaseExpansionOrRuleset, + StoredRules, } from "types/HomebrewCollection.type"; +export interface HomebrewEntry { + base: BaseExpansionOrRuleset; + rules?: { + data?: StoredRules; + loaded: boolean; + error?: string; + }; +} + export interface HomebrewSliceData { - collections: Record; + collections: Record; loading: boolean; error?: string; } export interface HomebrewSliceActions { subscribe: (uid: string) => Unsubscribe; + subscribeToHomebrewContent: (homebrewIds: string[]) => Unsubscribe; createExpansion: (expansion: BaseExpansion) => Promise; updateExpansion: ( diff --git a/src/stores/homebrew/useListenToHomebrewContent.ts b/src/stores/homebrew/useListenToHomebrewContent.ts new file mode 100644 index 00000000..4c3b9836 --- /dev/null +++ b/src/stores/homebrew/useListenToHomebrewContent.ts @@ -0,0 +1,15 @@ +import { useEffect } from "react"; +import { useStore } from "stores/store"; + +export function useListenToHomebrewContent(homebrewIds: string[]) { + const subscribeToHomebrewContent = useStore( + (store) => store.homebrew.subscribeToHomebrewContent + ); + useEffect(() => { + const unsubscribe = subscribeToHomebrewContent(homebrewIds); + + return () => { + unsubscribe(); + }; + }, [homebrewIds, subscribeToHomebrewContent]); +} diff --git a/src/types/HomebrewCollection.type.ts b/src/types/HomebrewCollection.type.ts index 44c31bf7..5b3fda4b 100644 --- a/src/types/HomebrewCollection.type.ts +++ b/src/types/HomebrewCollection.type.ts @@ -1,11 +1,4 @@ -import { - ConditionMeterRule, - Expansion, - ImpactCategory, - Ruleset, - SpecialTrackRule, - StatRule, -} from "@datasworn/core"; +import { Expansion, Ruleset } from "@datasworn/core"; type RuleKeys = | "oracles" @@ -38,12 +31,38 @@ export interface StoredRules { }; }; condition_meters: { - [conditionMeterKey: string]: ConditionMeterRule; + [conditionMeterKey: string]: { + description: string; + shared: boolean; + label: string; + value: number; + min: number; + max: number; + rollable: boolean; + }; }; impacts: { - [impactKey: string]: ImpactCategory; + [impactCategoryKey: string]: { + label: string; + description: string; + contents: { + [impactKey: string]: { + label: string; + description: string; + shared: boolean; + // ex: health, spirit + prevents_recovery: string[]; + permanent: boolean; + }; + }; + }; }; - specialTracks: { - [trackKey: string]: SpecialTrackRule; + special_tracks: { + [trackKey: string]: { + label: string; + description: string; + shared: boolean; + optional: boolean; + }; }; } From b140fc245b4d54d8ca0701f544e1de50aa36ec52 Mon Sep 17 00:00:00 2001 From: Scott Date: Sat, 16 Dec 2023 15:32:54 -0500 Subject: [PATCH 4/9] feat(rules): Got stats addition working --- package-lock.json | 35 +++++ package.json | 5 +- src/api-calls/homebrew/rules/_getRef.ts | 2 +- .../homebrew/rules/createHomebrewRules.ts | 2 +- .../homebrew/rules/listenToHomebrewRules.ts | 2 +- ...mebrewRules.ts => updateExpansionRules.ts} | 6 +- src/data/hooks/useRules.ts | 5 + src/data/rules.ts | 13 ++ src/functions/dataswornIdEncoder.ts | 2 - .../RulesSection/RulesSection.tsx | 2 +- .../HomebrewEditorPage/RulesSection/Stats.tsx | 85 ---------- .../RulesSection/Stats/StatDialog.tsx | 147 ++++++++++++++++++ .../RulesSection/Stats/Stats.tsx | 91 +++++++++++ .../RulesSection/Stats/index.ts | 1 + src/stores/homebrew/homebrew.slice.ts | 5 + src/stores/homebrew/homebrew.slice.type.ts | 9 +- src/types/HomebrewCollection.type.ts | 73 +++++---- 17 files changed, 354 insertions(+), 131 deletions(-) rename src/api-calls/homebrew/rules/{updateHomebrewRules.ts => updateExpansionRules.ts} (71%) create mode 100644 src/data/hooks/useRules.ts create mode 100644 src/data/rules.ts delete mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/index.ts diff --git a/package-lock.json b/package-lock.json index 8b5d1d05..48cb9901 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,8 @@ "version": "2.2.0", "dependencies": { "@datasworn/core": "^0.0.5", + "@datasworn/ironsworn-classic": "^0.0.5", + "@datasworn/starforged": "^0.0.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@fontsource/bebas-neue": "^5.0.12", @@ -38,6 +40,7 @@ "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", + "react-hook-form": "^7.49.2", "react-markdown": "^8.0.4", "react-router-dom": "^6.6.1", "react-transition-group": "^4.4.5", @@ -447,6 +450,22 @@ "resolved": "https://registry.npmjs.org/@datasworn/core/-/core-0.0.5.tgz", "integrity": "sha512-rf3xad4Bbu1jsUgA1sp/Xn77jCmw8kFuT1IybSuR62VOq3C9vLc2v9+hybCLYuu2xJWAJVXtr7i6VqtBD+p+OA==" }, + "node_modules/@datasworn/ironsworn-classic": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@datasworn/ironsworn-classic/-/ironsworn-classic-0.0.5.tgz", + "integrity": "sha512-6RkJwb+Sr0MeBCtt/0x7WTSRbwt6YJXW5hHWLrsk9UnmbP3Sfoltks9Z4An9qrbvpOv6Cu+WsVRoUBAJpsVIkw==", + "dependencies": { + "@datasworn/core": "0.0.5" + } + }, + "node_modules/@datasworn/starforged": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/@datasworn/starforged/-/starforged-0.0.5.tgz", + "integrity": "sha512-N/lWSR91lpZIwZl4VZa8QTBj3qEsmVMoJSo5bY2R6ZZoUkzvBo7VBp0woMGV8MVglHOWD6tqdKyO1qeVt8awQA==", + "dependencies": { + "@datasworn/core": "0.0.5" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.11.0", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", @@ -7378,6 +7397,22 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-hook-form": { + "version": "7.49.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.49.2.tgz", + "integrity": "sha512-TZcnSc17+LPPVpMRIDNVITY6w20deMdNi6iehTFLV1x8SqThXGwu93HjlUVU09pzFgZH7qZOvLMM7UYf2ShAHA==", + "engines": { + "node": ">=18", + "pnpm": "8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", diff --git a/package.json b/package.json index 1feb6e9e..3bd99fe2 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,8 @@ }, "dependencies": { "@datasworn/core": "^0.0.5", + "@datasworn/ironsworn-classic": "^0.0.5", + "@datasworn/starforged": "^0.0.5", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@fontsource/bebas-neue": "^5.0.12", @@ -19,8 +21,8 @@ "@hocuspocus/transformer": "^2.4.0", "@mui/icons-material": "^5.14.15", "@mui/lab": "^5.0.0-alpha.150", - "@mui/x-date-pickers": "^6.18.4", "@mui/material": "^5.15.0", + "@mui/x-date-pickers": "^6.18.4", "@tiptap/extension-collaboration": "^2.0.3", "@tiptap/extension-collaboration-cursor": "^2.0.3", "@tiptap/extension-placeholder": "^2.0.0-beta.218", @@ -40,6 +42,7 @@ "react-beautiful-dnd": "^13.1.1", "react-dom": "^18.2.0", "react-helmet-async": "^1.3.0", + "react-hook-form": "^7.49.2", "react-markdown": "^8.0.4", "react-router-dom": "^6.6.1", "react-transition-group": "^4.4.5", diff --git a/src/api-calls/homebrew/rules/_getRef.ts b/src/api-calls/homebrew/rules/_getRef.ts index 0e305a58..74f98dd4 100644 --- a/src/api-calls/homebrew/rules/_getRef.ts +++ b/src/api-calls/homebrew/rules/_getRef.ts @@ -11,5 +11,5 @@ export function getHomebrewRulesDoc(homebrewId: string) { return doc( firestore, constructHomebrewRulesDocPath(homebrewId) - ) as DocumentReference; + ) as DocumentReference>; } diff --git a/src/api-calls/homebrew/rules/createHomebrewRules.ts b/src/api-calls/homebrew/rules/createHomebrewRules.ts index 001e3ba9..e07684dc 100644 --- a/src/api-calls/homebrew/rules/createHomebrewRules.ts +++ b/src/api-calls/homebrew/rules/createHomebrewRules.ts @@ -6,7 +6,7 @@ import { StoredRules } from "types/HomebrewCollection.type"; export const updateHomebrewRules = createApiFunction< { homebrewId: string; - rules: PartialWithFieldValue; + rules: PartialWithFieldValue>; }, void >((params) => { diff --git a/src/api-calls/homebrew/rules/listenToHomebrewRules.ts b/src/api-calls/homebrew/rules/listenToHomebrewRules.ts index 8e3b980f..83c16dc1 100644 --- a/src/api-calls/homebrew/rules/listenToHomebrewRules.ts +++ b/src/api-calls/homebrew/rules/listenToHomebrewRules.ts @@ -4,7 +4,7 @@ import { getHomebrewRulesDoc } from "./_getRef"; export function listenToHomebrewRules( homebrewId: string, - updateRules: (collectionId: string, rules: StoredRules) => void, + updateRules: (collectionId: string, rules: Partial) => void, onError: (error: unknown) => void, onLoaded: () => void ) { diff --git a/src/api-calls/homebrew/rules/updateHomebrewRules.ts b/src/api-calls/homebrew/rules/updateExpansionRules.ts similarity index 71% rename from src/api-calls/homebrew/rules/updateHomebrewRules.ts rename to src/api-calls/homebrew/rules/updateExpansionRules.ts index 001e3ba9..79c59d91 100644 --- a/src/api-calls/homebrew/rules/updateHomebrewRules.ts +++ b/src/api-calls/homebrew/rules/updateExpansionRules.ts @@ -1,9 +1,9 @@ import { createApiFunction } from "api-calls/createApiFunction"; -import { PartialWithFieldValue, updateDoc } from "firebase/firestore"; +import { PartialWithFieldValue, setDoc } from "firebase/firestore"; import { getHomebrewRulesDoc } from "./_getRef"; import { StoredRules } from "types/HomebrewCollection.type"; -export const updateHomebrewRules = createApiFunction< +export const updateExpansionRules = createApiFunction< { homebrewId: string; rules: PartialWithFieldValue; @@ -12,7 +12,7 @@ export const updateHomebrewRules = createApiFunction< >((params) => { const { homebrewId, rules } = params; return new Promise((resolve, reject) => { - updateDoc(getHomebrewRulesDoc(homebrewId), rules) + setDoc(getHomebrewRulesDoc(homebrewId), rules, { merge: true }) .then(() => { resolve(); }) diff --git a/src/data/hooks/useRules.ts b/src/data/hooks/useRules.ts new file mode 100644 index 00000000..2176e1c3 --- /dev/null +++ b/src/data/hooks/useRules.ts @@ -0,0 +1,5 @@ +import { rules } from "data/rules"; + +export function useRules() { + return rules; +} diff --git a/src/data/rules.ts b/src/data/rules.ts new file mode 100644 index 00000000..295b77e6 --- /dev/null +++ b/src/data/rules.ts @@ -0,0 +1,13 @@ +import { Rules } from "@datasworn/core"; +import { rules as ironswornRules } from "@datasworn/ironsworn-classic/json/classic.json"; +import { rules as starforgedRules } from "@datasworn/starforged/json/starforged.json"; +import { getGameSystem } from "functions/getGameSystem"; +import { GAME_SYSTEMS, GameSystemChooser } from "types/GameSystems.type"; + +const gameSystem = getGameSystem(); +const rulesMap: GameSystemChooser = { + [GAME_SYSTEMS.IRONSWORN]: ironswornRules as Rules, + [GAME_SYSTEMS.STARFORGED]: starforgedRules as Rules, +}; + +export const rules = rulesMap[gameSystem]; diff --git a/src/functions/dataswornIdEncoder.ts b/src/functions/dataswornIdEncoder.ts index 3b72c6e3..72a1c0e0 100644 --- a/src/functions/dataswornIdEncoder.ts +++ b/src/functions/dataswornIdEncoder.ts @@ -74,6 +74,4 @@ export function convertIdPart( throw new Error( `Failed to create valid ID: ID Part = ${idPart}, New ID Part = ${newIdPart}` ); - - return newIdPart; } diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx index 7fa667c8..0b0c4f32 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx @@ -30,7 +30,7 @@ export function RulesSection(props: RulesSectionProps) { }} > - + diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats.tsx deleted file mode 100644 index c0428d22..00000000 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import { - Box, - Button, - Dialog, - DialogActions, - DialogContent, - Stack, - TextField, - Typography, -} from "@mui/material"; -import { StatComponent } from "components/features/characters/StatComponent"; -import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; -import { useState } from "react"; -import { StoredRules } from "types/HomebrewCollection.type"; - -export interface StatsProps { - stats: StoredRules["stats"]; -} - -export function Stats(props: StatsProps) { - const { stats } = props; - - const [statDialogOpen, setStatDialogOpen] = useState(false); - const [editingStatKey, setEditingStatKey] = useState(); - - const [editingStatLabel, setEditingStatLabel] = useState(""); - const [editingStatDescription, setEditingStatDescription] = useState(""); - - return ( - <> - {Object.keys(stats).length === 0 && ( - No Stats Found - )} - {Object.keys(stats).map((statKey) => ( - {stats[statKey].label} - ))} - - setStatDialogOpen(false)}> - setStatDialogOpen(false)}> - {editingStatKey ? "Edit" : "Add"} Stat - - - - setEditingStatLabel(evt.currentTarget.value)} - /> - - setEditingStatDescription(evt.currentTarget.value) - } - /> - - Example - - - - - - - - - - - ); -} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx new file mode 100644 index 00000000..3c287cd0 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx @@ -0,0 +1,147 @@ +import { StoredRules, StoredStat } from "types/HomebrewCollection.type"; +import { useForm, SubmitHandler } from "react-hook-form"; +import { + Button, + Dialog, + DialogActions, + DialogContent, + Stack, + TextField, +} from "@mui/material"; +import { useRules } from "data/hooks/useRules"; +import { convertIdPart } from "functions/dataswornIdEncoder"; +import { useEffect, useState } from "react"; +import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; + +export interface StatDialogProps { + stats: StoredRules["stats"]; + open: boolean; + onSave: (statId: string, stat: StoredStat) => Promise; + onClose: () => void; + editingStatKey?: string; +} + +interface StatInputs { + label: string; + description: string; +} + +export function StatDialog(props: StatDialogProps) { + const { stats, open, onSave, onClose, editingStatKey } = props; + + const existingStat = editingStatKey + ? stats[editingStatKey] ?? undefined + : undefined; + const { stats: baseStats } = useRules(); + const existingStatKeys = Object.keys({ ...stats, ...baseStats }); + + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, touchedFields, disabled }, + reset, + } = useForm({ disabled: loading }); + + useEffect(() => { + if (open) { + reset( + existingStat + ? { + label: existingStat.label, + description: existingStat.description, + } + : undefined + ); + } + }, [open, reset, existingStat]); + + const onSubmit: SubmitHandler = (values) => { + setLoading(true); + const id = editingStatKey ?? convertIdPart(values.label); + onSave(id, values) + .then(() => { + setLoading(false); + onClose(); + }) + .catch(() => { + setLoading(false); + }); + }; + + return ( + + + {existingStat ? "Edit Stat" : "Add Stat"} + +
+ + + {/* register your input into the hook by invoking the "register" function */} + { + if (!editingStatKey && value) { + try { + const id = convertIdPart(value); + console.debug(id); + if (existingStatKeys.includes(id)) { + return `You already have a stat with id ${id}. Please try a different label.`; + } + } catch (e) { + return "Failed to parse a valid ID for your stat. Please use at least three letters or numbers in your label."; + } + } + }, + }), + }} + /> + + + + + + + +
+
+ ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx new file mode 100644 index 00000000..bb518172 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx @@ -0,0 +1,91 @@ +import { Button, Chip, Stack, Typography } from "@mui/material"; +import { useState } from "react"; +import { StoredRules, StoredStat } from "types/HomebrewCollection.type"; +import { StatDialog } from "./StatDialog"; +import { useStore } from "stores/store"; +import { deleteField } from "firebase/firestore"; +import { useConfirm } from "material-ui-confirm"; +import EditIcon from "@mui/icons-material/Edit"; + +export interface StatsProps { + homebrewId: string; + stats: StoredRules["stats"]; +} + +export function Stats(props: StatsProps) { + const { homebrewId, stats } = props; + + const confirm = useConfirm(); + + const [statDialogOpen, setStatDialogOpen] = useState(false); + const [editingStatKey, setEditingStatKey] = useState( + undefined + ); + + const updateRules = useStore((store) => store.homebrew.updateExpansionRules); + const addStat = (statId: string, stat: StoredStat) => { + return updateRules(homebrewId, { stats: { [statId]: stat } }); + }; + const deleteStat = (statId: string) => { + return updateRules(homebrewId, { stats: { [statId]: deleteField() } }); + }; + + const handleDelete = (statId: string) => { + confirm({ + title: `Delete ${stats[statId].label}`, + description: + "Are you sure you want to delete this custom stat? It will be deleted from ALL of your custom content that references this stat.", + confirmationText: "Delete", + confirmationButtonProps: { + variant: "contained", + color: "error", + }, + }) + .then(() => { + deleteStat(statId).catch(() => {}); + }) + .catch(() => {}); + }; + + return ( + <> + {Object.keys(stats).length === 0 && ( + No Stats Found + )} + + {Object.keys(stats) + .sort((s1, s2) => stats[s1].label.localeCompare(stats[s2].label)) + .map((statKey) => ( + handleDelete(statKey)} + icon={} + onClick={() => { + setStatDialogOpen(true); + setEditingStatKey(statKey); + }} + /> + ))} + + + setStatDialogOpen(false)} + stats={stats} + editingStatKey={editingStatKey} + /> + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/index.ts b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/index.ts new file mode 100644 index 00000000..30942e03 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/index.ts @@ -0,0 +1 @@ +export * from "./Stats"; diff --git a/src/stores/homebrew/homebrew.slice.ts b/src/stores/homebrew/homebrew.slice.ts index f3c99177..af20b664 100644 --- a/src/stores/homebrew/homebrew.slice.ts +++ b/src/stores/homebrew/homebrew.slice.ts @@ -8,6 +8,7 @@ import { updateHomebrewExpansion } from "api-calls/homebrew/updateHomebrewExpans import { deleteHomebrewExpansion } from "api-calls/homebrew/deleteHomebrewExpansion"; import { Unsubscribe } from "firebase/firestore"; import { listenToHomebrewRules } from "api-calls/homebrew/rules/listenToHomebrewRules"; +import { updateExpansionRules } from "api-calls/homebrew/rules/updateExpansionRules"; export const createHomebrewSlice: CreateSliceType = (set) => ({ ...defaultHomebrewSlice, @@ -106,4 +107,8 @@ export const createHomebrewSlice: CreateSliceType = (set) => ({ deleteExpansion: (id) => { return deleteHomebrewExpansion({ id }); }, + + updateExpansionRules: (homebrewId, rules) => { + return updateExpansionRules({ homebrewId, rules }); + }, }); diff --git a/src/stores/homebrew/homebrew.slice.type.ts b/src/stores/homebrew/homebrew.slice.type.ts index 108f2d3a..78f6dbec 100644 --- a/src/stores/homebrew/homebrew.slice.type.ts +++ b/src/stores/homebrew/homebrew.slice.type.ts @@ -1,4 +1,4 @@ -import { Unsubscribe } from "firebase/firestore"; +import { PartialWithFieldValue, Unsubscribe } from "firebase/firestore"; import { BaseExpansion, BaseExpansionOrRuleset, @@ -8,7 +8,7 @@ import { export interface HomebrewEntry { base: BaseExpansionOrRuleset; rules?: { - data?: StoredRules; + data?: Partial; loaded: boolean; error?: string; }; @@ -30,6 +30,11 @@ export interface HomebrewSliceActions { expansion: Partial ) => Promise; deleteExpansion: (expansionId: string) => Promise; + + updateExpansionRules: ( + expansionId: string, + rules: PartialWithFieldValue + ) => Promise; } export type HomebrewSlice = HomebrewSliceData & HomebrewSliceActions; diff --git a/src/types/HomebrewCollection.type.ts b/src/types/HomebrewCollection.type.ts index 5b3fda4b..0de4a266 100644 --- a/src/types/HomebrewCollection.type.ts +++ b/src/types/HomebrewCollection.type.ts @@ -23,46 +23,51 @@ export type BaseRuleset = Omit & additions; export type BaseExpansionOrRuleset = BaseExpansion | BaseRuleset; -export interface StoredRules { - stats: { - [statKey: string]: { +export interface StoredStat { + label: string; + description: string; +} + +export interface StoredConditionMeter { + description: string; + shared: boolean; + label: string; + value: number; + min: number; + max: number; + rollable: boolean; +} + +export interface StoredImpact { + label: string; + description: string; + contents: { + [impactKey: string]: { label: string; description: string; + shared: boolean; + // ex: health, spirit + prevents_recovery: string[]; + permanent: boolean; }; }; +} +export interface StoredSpecialTrack { + label: string; + description: string; + shared: boolean; + optional: boolean; +} + +export interface StoredRules { + stats: { + [statKey: string]: StoredStat; + }; condition_meters: { - [conditionMeterKey: string]: { - description: string; - shared: boolean; - label: string; - value: number; - min: number; - max: number; - rollable: boolean; - }; + [conditionMeterKey: string]: StoredConditionMeter; }; impacts: { - [impactCategoryKey: string]: { - label: string; - description: string; - contents: { - [impactKey: string]: { - label: string; - description: string; - shared: boolean; - // ex: health, spirit - prevents_recovery: string[]; - permanent: boolean; - }; - }; - }; - }; - special_tracks: { - [trackKey: string]: { - label: string; - description: string; - shared: boolean; - optional: boolean; - }; + [impactCategoryKey: string]: StoredImpact; }; + special_tracks: { [specialTrackKey: string]: StoredSpecialTrack }; } From 6701296cffc42898b768fb7024691a1da440ef64 Mon Sep 17 00:00:00 2001 From: Scott Date: Sun, 17 Dec 2023 16:24:51 -0500 Subject: [PATCH 5/9] feat(homebrew): Added condition meters section --- src/components/features/Track.tsx | 11 +- .../Homebrew/HomebrewEditorPage/Preview.tsx | 18 ++ .../ConditionMeters/ConditionMeterDialog.tsx | 253 ++++++++++++++++++ .../ConditionMeters/ConditionMeterPreview.tsx | 37 +++ .../ConditionMeters/ConditionMeters.tsx | 132 +++++++++ .../RulesSection/ConditionMeters/index.ts | 1 + .../RulesSection/RulesSection.tsx | 3 + .../RulesSection/Stats/StatDialog.tsx | 15 +- .../Stats/StatPreviewComponent.tsx | 17 ++ .../RulesSection/Stats/Stats.tsx | 64 +++-- 10 files changed, 523 insertions(+), 28 deletions(-) create mode 100644 src/pages/Homebrew/HomebrewEditorPage/Preview.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterPreview.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeters.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/index.ts create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatPreviewComponent.tsx diff --git a/src/components/features/Track.tsx b/src/components/features/Track.tsx index 3959181e..a4e986df 100644 --- a/src/components/features/Track.tsx +++ b/src/components/features/Track.tsx @@ -8,7 +8,7 @@ export interface TrackProps { min: number; max: number; value: number; - onChange: (newValue: number) => Promise; + onChange?: (newValue: number) => Promise; sx?: SxProps; disabled?: boolean; } @@ -32,7 +32,7 @@ export function Track(props: TrackProps) { const [localValue, setLocalValue] = useDebouncedState( (newValue) => { - if (newValue !== value) { + if (newValue !== value && onChange) { hasUnsavedChangesRef.current = false; onChange(newValue).catch(() => setLocalValue(value)); } @@ -42,7 +42,12 @@ export function Track(props: TrackProps) { ); const handleChange = (newValue: number | undefined) => { - if (typeof newValue === "number" && newValue >= min && newValue <= max) { + if ( + onChange && + typeof newValue === "number" && + newValue >= min && + newValue <= max + ) { hasUnsavedChangesRef.current = true; setLocalValue(newValue); } diff --git a/src/pages/Homebrew/HomebrewEditorPage/Preview.tsx b/src/pages/Homebrew/HomebrewEditorPage/Preview.tsx new file mode 100644 index 00000000..6d7bd754 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/Preview.tsx @@ -0,0 +1,18 @@ +import { Stack, Typography } from "@mui/material"; +import { PropsWithChildren } from "react"; + +// eslint-disable-next-line @typescript-eslint/ban-types +export type PreviewProps = PropsWithChildren<{}>; + +export function Preview(props: PreviewProps) { + const { children } = props; + return ( + + Preview + {children} + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx new file mode 100644 index 00000000..1d573ed9 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx @@ -0,0 +1,253 @@ +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + FormControl, + FormControlLabel, + FormHelperText, + Stack, + TextField, +} from "@mui/material"; +import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; +import { useRules } from "data/hooks/useRules"; +import { useEffect, useState } from "react"; +import { + StoredConditionMeter, + StoredRules, +} from "types/HomebrewCollection.type"; +import { useForm, SubmitHandler, Controller } from "react-hook-form"; +import { convertIdPart } from "functions/dataswornIdEncoder"; +import { Preview } from "../../Preview"; +import { ConditionMeterPreview } from "./ConditionMeterPreview"; + +export interface ConditionMeterDialogProps { + conditionMeters: StoredRules["condition_meters"]; + open: boolean; + onSave: ( + conditionMeterId: string, + conditionMeter: StoredConditionMeter + ) => Promise; + onClose: () => void; + editingConditionMeterKey?: string; +} + +export function ConditionMeterDialog(props: ConditionMeterDialogProps) { + const { open, onClose, onSave, conditionMeters, editingConditionMeterKey } = + props; + + const existingConditionMeter = editingConditionMeterKey + ? conditionMeters[editingConditionMeterKey] ?? undefined + : undefined; + + const { condition_meters: baseConditionMeters } = useRules(); + const allConditionMeters = { ...baseConditionMeters, ...conditionMeters }; + + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, touchedFields, disabled }, + reset, + control, + } = useForm({ disabled: loading }); + + useEffect(() => { + if (open) { + reset(existingConditionMeter ?? undefined); + } + }, [open, reset, existingConditionMeter]); + + const onSubmit: SubmitHandler = (values) => { + setLoading(true); + const id = editingConditionMeterKey ?? convertIdPart(values.label); + onSave(id, values) + .then(() => { + setLoading(false); + onClose(); + }) + .catch(() => { + setLoading(false); + }); + }; + + return ( + + + {editingConditionMeterKey + ? "Edit Condition Meter" + : "Add Condition Meter"} + +
+ + + { + if (!editingConditionMeterKey && value) { + try { + const id = convertIdPart(value); + console.debug(id); + if (allConditionMeters[id]) { + return `You already have a condition meter with id ${id}. Please try a different label.`; + } + } catch (e) { + return "Failed to parse a valid ID for your condition meter. Please use at least three letters or numbers in your label."; + } + } + }, + }), + }} + /> + { + if ( + typeof formValues.max === "number" && + formValues.max < formValues.min + ) { + return "Min should be less than the max"; + } + }, + }), + }} + /> + { + if ( + typeof formValues.min === "number" && + formValues.max < formValues.min + ) { + return "Max should be greater than than the min"; + } + }, + }), + }} + /> + { + if ( + typeof formValues.max === "number" && + typeof formValues.min === "number" && + (formValues.min > value || formValues.max < value) + ) { + return `Default value must be between ${formValues.min} and ${formValues.max}`; + } + }, + }), + }} + /> + + ( + + )} + /> + } + label={"Shared across all players?"} + /> + {touchedFields.shared && errors.shared && ( + {errors.shared.message} + )} + + + ( + + )} + /> + } + label={"Add a roller for this stat?"} + /> + {touchedFields.rollable && errors.rollable && ( + {errors.rollable.message} + )} + + + + + + + + + + +
+
+ ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterPreview.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterPreview.tsx new file mode 100644 index 00000000..48154cbd --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterPreview.tsx @@ -0,0 +1,37 @@ +import { Typography } from "@mui/material"; +import { Track } from "components/features/Track"; +import { StatComponent } from "components/features/characters/StatComponent"; +import { Control, useWatch } from "react-hook-form"; +import { StoredConditionMeter } from "types/HomebrewCollection.type"; + +export interface ConditionMeterPreviewProps { + control: Control; +} + +export function ConditionMeterPreview(props: ConditionMeterPreviewProps) { + const { control } = props; + + const label = useWatch({ control, name: "label" }); + const min = useWatch({ control, name: "min" }); + const max = useWatch({ control, name: "max" }); + const defaultValue = useWatch({ control, name: "value" }); + const rollable = useWatch({ control, name: "rollable" }); + + if ( + !label || + min === undefined || + max === undefined || + defaultValue === undefined + ) { + return Waiting for input...; + } + + return ( + <> + + {rollable && ( + + )} + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeters.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeters.tsx new file mode 100644 index 00000000..b9ebe99e --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeters.tsx @@ -0,0 +1,132 @@ +import { + Button, + IconButton, + List, + ListItem, + ListItemText, + Typography, +} from "@mui/material"; +import { + StoredConditionMeter, + StoredRules, +} from "types/HomebrewCollection.type"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useState } from "react"; +import { useStore } from "stores/store"; +import { deleteField } from "firebase/firestore"; +import { useConfirm } from "material-ui-confirm"; +import { ConditionMeterDialog } from "./ConditionMeterDialog"; + +export interface ConditionMetersProps { + homebrewId: string; + conditionMeters: StoredRules["condition_meters"]; +} + +export function ConditionMeters(props: ConditionMetersProps) { + const { homebrewId, conditionMeters } = props; + + const confirm = useConfirm(); + + const [conditionMeterDialogOpen, setConditionMeterDialogOpen] = + useState(false); + const [editingConditionMeterKey, setEditingConditionMeterKey] = useState< + string | undefined + >(undefined); + + const updateRules = useStore((store) => store.homebrew.updateExpansionRules); + const addConditionMeter = ( + conditionMeterId: string, + conditionMeter: StoredConditionMeter + ) => { + return updateRules(homebrewId, { + condition_meters: { [conditionMeterId]: conditionMeter }, + }); + }; + const deleteConditionMeter = (conditionMeterId: string) => { + return updateRules(homebrewId, { + condition_meters: { [conditionMeterId]: deleteField() }, + }); + }; + + const handleDelete = (conditionMeterId: string) => { + confirm({ + title: `Delete ${conditionMeters[conditionMeterId].label}`, + description: + "Are you sure you want to delete this conditon meter? It will be deleted from ALL of your custom content that references this meter.", + confirmationText: "Delete", + confirmationButtonProps: { + variant: "contained", + color: "error", + }, + }) + .then(() => { + deleteConditionMeter(conditionMeterId).catch(() => {}); + }) + .catch(() => {}); + }; + + return ( + <> + {Object.keys(conditionMeters).length === 0 ? ( + + No Condition Meters Found + + ) : ( + + {Object.keys(conditionMeters).map((conditionMeterKey) => ( + + { + setConditionMeterDialogOpen(true); + setEditingConditionMeterKey(conditionMeterKey); + }} + > + + + handleDelete(conditionMeterKey)}> + + + + } + > + + + ))} + + )} + + setConditionMeterDialogOpen(false)} + onSave={addConditionMeter} + editingConditionMeterKey={editingConditionMeterKey} + /> + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/index.ts b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/index.ts new file mode 100644 index 00000000..7eb22bc1 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/index.ts @@ -0,0 +1 @@ +export * from "./ConditionMeters"; diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx index 0b0c4f32..da47fb04 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx @@ -2,6 +2,7 @@ import { Box, LinearProgress } from "@mui/material"; import { SectionHeading } from "components/shared/SectionHeading"; import { useStore } from "stores/store"; import { Stats } from "./Stats"; +import { ConditionMeters } from "./ConditionMeters"; export interface RulesSectionProps { id: string; @@ -16,6 +17,7 @@ export function RulesSection(props: RulesSectionProps) { const rules = useStore((store) => store.homebrew.collections[id].rules?.data); const stats = rules?.stats ?? {}; + const conditonMeters = rules?.condition_meters ?? {}; if (loading) { return ; @@ -32,6 +34,7 @@ export function RulesSection(props: RulesSectionProps) { + diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx index 3c287cd0..33e41628 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx @@ -12,6 +12,8 @@ import { useRules } from "data/hooks/useRules"; import { convertIdPart } from "functions/dataswornIdEncoder"; import { useEffect, useState } from "react"; import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; +import { Preview } from "../../Preview"; +import { StatPreviewComponent } from "./StatPreviewComponent"; export interface StatDialogProps { stats: StoredRules["stats"]; @@ -21,11 +23,6 @@ export interface StatDialogProps { editingStatKey?: string; } -interface StatInputs { - label: string; - description: string; -} - export function StatDialog(props: StatDialogProps) { const { stats, open, onSave, onClose, editingStatKey } = props; @@ -40,9 +37,10 @@ export function StatDialog(props: StatDialogProps) { const { register, handleSubmit, + control, formState: { errors, touchedFields, disabled }, reset, - } = useForm({ disabled: loading }); + } = useForm({ disabled: loading }); useEffect(() => { if (open) { @@ -57,7 +55,7 @@ export function StatDialog(props: StatDialogProps) { } }, [open, reset, existingStat]); - const onSubmit: SubmitHandler = (values) => { + const onSubmit: SubmitHandler = (values) => { setLoading(true); const id = editingStatKey ?? convertIdPart(values.label); onSave(id, values) @@ -126,6 +124,9 @@ export function StatDialog(props: StatDialogProps) { }), }} /> + + + diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatPreviewComponent.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatPreviewComponent.tsx new file mode 100644 index 00000000..99f1d80c --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatPreviewComponent.tsx @@ -0,0 +1,17 @@ +import { Control, useWatch } from "react-hook-form"; +import { StatComponent } from "components/features/characters/StatComponent"; +import { StoredStat } from "types/HomebrewCollection.type"; +export interface StatPreviewComponentProps { + control: Control; +} +export function StatPreviewComponent(props: StatPreviewComponentProps) { + const { control } = props; + + const label = useWatch({ + control, + name: "label", + defaultValue: "Label", + }); + + return ; +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx index bb518172..2b5dec88 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx @@ -1,4 +1,11 @@ -import { Button, Chip, Stack, Typography } from "@mui/material"; +import { + Button, + IconButton, + List, + ListItem, + ListItemText, + Typography, +} from "@mui/material"; import { useState } from "react"; import { StoredRules, StoredStat } from "types/HomebrewCollection.type"; import { StatDialog } from "./StatDialog"; @@ -6,6 +13,7 @@ import { useStore } from "stores/store"; import { deleteField } from "firebase/firestore"; import { useConfirm } from "material-ui-confirm"; import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; export interface StatsProps { homebrewId: string; @@ -49,26 +57,46 @@ export function Stats(props: StatsProps) { return ( <> - {Object.keys(stats).length === 0 && ( + {Object.keys(stats).length === 0 ? ( No Stats Found - )} - - {Object.keys(stats) - .sort((s1, s2) => stats[s1].label.localeCompare(stats[s2].label)) - .map((statKey) => ( - + {Object.keys(stats).map((statKey) => ( + handleDelete(statKey)} - icon={} - onClick={() => { - setStatDialogOpen(true); - setEditingStatKey(statKey); - }} - /> + sx={{ gridColumn: { xs: "span 12", sm: "span 6", md: "span 4" } }} + secondaryAction={ + <> + { + setStatDialogOpen(true); + setEditingStatKey(statKey); + }} + > + + + handleDelete(statKey)}> + + + + } + > + + ))} - + + )} + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/index.ts b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/index.ts new file mode 100644 index 00000000..335c91da --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/index.ts @@ -0,0 +1 @@ +export * from "./Impacts"; diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx index da47fb04..600325af 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx @@ -3,6 +3,7 @@ import { SectionHeading } from "components/shared/SectionHeading"; import { useStore } from "stores/store"; import { Stats } from "./Stats"; import { ConditionMeters } from "./ConditionMeters"; +import { Impacts } from "./Impacts"; export interface RulesSectionProps { id: string; @@ -18,6 +19,7 @@ export function RulesSection(props: RulesSectionProps) { const rules = useStore((store) => store.homebrew.collections[id].rules?.data); const stats = rules?.stats ?? {}; const conditonMeters = rules?.condition_meters ?? {}; + const impacts = rules?.impacts ?? {}; if (loading) { return ; @@ -36,6 +38,7 @@ export function RulesSection(props: RulesSectionProps) { + ); From 58700dfc084320c04e2df1a64da8689ff150a66c Mon Sep 17 00:00:00 2001 From: Scott Date: Mon, 18 Dec 2023 17:36:32 -0500 Subject: [PATCH 7/9] feat(homebrew): Created a markdown editor to use with homebrew content --- package-lock.json | 39 ++++++++ package.json | 1 + .../shared/RichTextEditor/MarkdownEditor.tsx | 62 ++++++++++++ .../RichTextEditor/MarkdownEditorToolbar.tsx | 95 +++++++++++++++++++ .../RulesSection/Stats/StatDialog.tsx | 31 +++--- .../RulesSection/Stats/Stats.tsx | 5 +- 6 files changed, 212 insertions(+), 21 deletions(-) create mode 100644 src/components/shared/RichTextEditor/MarkdownEditor.tsx create mode 100644 src/components/shared/RichTextEditor/MarkdownEditorToolbar.tsx diff --git a/package-lock.json b/package-lock.json index 48cb9901..c1bd09c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -46,6 +46,7 @@ "react-transition-group": "^4.4.5", "react-virtuoso": "^4.5.0", "remark-gfm": "^3.0.1", + "tiptap-markdown": "^0.8.8", "vite-plugin-svgr": "^2.4.0", "vite-tsconfig-paths": "^4.0.5", "y-prosemirror": "1.0.20", @@ -2895,6 +2896,20 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/linkify-it": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.5.tgz", + "integrity": "sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==" + }, + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, "node_modules/@types/mdast": { "version": "3.0.14", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.14.tgz", @@ -2903,6 +2918,11 @@ "@types/unist": "^2" } }, + "node_modules/@types/mdurl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.5.tgz", + "integrity": "sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==" + }, "node_modules/@types/ms": { "version": "0.7.33", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.33.tgz", @@ -5853,6 +5873,11 @@ "markdown-it": "bin/markdown-it.js" } }, + "node_modules/markdown-it-task-lists": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/markdown-it-task-lists/-/markdown-it-task-lists-2.1.1.tgz", + "integrity": "sha512-TxFAc76Jnhb2OUu+n3yz9RMu4CwGfaT788br6HhEDlvWfdeJcLUsxk1Hgw2yJio0OXsxv7pyIPmvECY7bMbluA==" + }, "node_modules/markdown-table": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.3.tgz", @@ -8150,6 +8175,20 @@ "@popperjs/core": "^2.9.0" } }, + "node_modules/tiptap-markdown": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/tiptap-markdown/-/tiptap-markdown-0.8.8.tgz", + "integrity": "sha512-I2w/IpvCZ1BoR3nQzG0wRK3uGmDv+Ohyr++G24Ma6RzoDYd0TVGXZp0BOODX5Jj4c6heVY8eksahSeAwJMZBeg==", + "dependencies": { + "@types/markdown-it": "^12.2.3", + "markdown-it": "^13.0.1", + "markdown-it-task-lists": "^2.1.1", + "prosemirror-markdown": "^1.11.1" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.3" + } + }, "node_modules/to-fast-properties": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", diff --git a/package.json b/package.json index 3bd99fe2..508b9fb6 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "react-transition-group": "^4.4.5", "react-virtuoso": "^4.5.0", "remark-gfm": "^3.0.1", + "tiptap-markdown": "^0.8.8", "vite-plugin-svgr": "^2.4.0", "vite-tsconfig-paths": "^4.0.5", "y-prosemirror": "1.0.20", diff --git a/src/components/shared/RichTextEditor/MarkdownEditor.tsx b/src/components/shared/RichTextEditor/MarkdownEditor.tsx new file mode 100644 index 00000000..424f0085 --- /dev/null +++ b/src/components/shared/RichTextEditor/MarkdownEditor.tsx @@ -0,0 +1,62 @@ +import { useEditor } from "@tiptap/react"; +import StarterKit from "@tiptap/starter-kit"; +import { Markdown } from "tiptap-markdown"; +import { Editor } from "./Editor"; +import { Box, Typography } from "@mui/material"; +import { MarkdownEditorToolbar } from "./MarkdownEditorToolbar"; +export interface MarkdownEditorProps { + label: string; + content: string; + onChange: (markdown: string) => void; + onBlur: () => void; +} + +export function MarkdownEditor(props: MarkdownEditorProps) { + const { label, content, onChange, onBlur } = props; + + const editor = useEditor( + { + extensions: [StarterKit, Markdown], + content, + onBlur, + onUpdate: ({ editor }) => { + const markdown = editor.storage.markdown.getMarkdown(); + onChange(markdown); + }, + }, + [] + ); + + return ( + + ({ + top: theme.spacing(-1.5), + left: theme.spacing(1), + })} + > + + {label} + + + + + } + editor={editor} + editable + outlined + /> + ); +} diff --git a/src/components/shared/RichTextEditor/MarkdownEditorToolbar.tsx b/src/components/shared/RichTextEditor/MarkdownEditorToolbar.tsx new file mode 100644 index 00000000..a6e8ee91 --- /dev/null +++ b/src/components/shared/RichTextEditor/MarkdownEditorToolbar.tsx @@ -0,0 +1,95 @@ +import { Box, ToggleButton, ToggleButtonGroup, Tooltip } from "@mui/material"; +import { Editor } from "@tiptap/react"; +import BoldIcon from "@mui/icons-material/FormatBold"; +import ItalicIcon from "@mui/icons-material/FormatItalic"; +import StrikeThroughIcon from "@mui/icons-material/FormatStrikethrough"; +import HorizontalRuleIcon from "@mui/icons-material/HorizontalRule"; +import BulletListIcon from "@mui/icons-material/FormatListBulleted"; +import NumberedListIcon from "@mui/icons-material/FormatListNumbered"; + +export interface MarkdownEditorToolbarProps { + editor: Editor | null; +} + +export function MarkdownEditorToolbar(props: MarkdownEditorToolbarProps) { + const { editor } = props; + + if (!editor) { + return null; + } + + return ( + `1px solid ${theme.palette.divider}`} + px={2} + mt={-1} + pb={1} + position={"sticky"} + top={0} + bgcolor={(theme) => theme.palette.background.paper} + zIndex={2} + > + + + editor.chain().focus().toggleBold().run()} + selected={editor.isActive("bold")} + > + + + + + + editor.chain().focus().toggleItalic().run()} + selected={editor.isActive("italic")} + > + + + + + + editor.chain().focus().toggleStrike().run()} + selected={editor.isActive("strike")} + > + + + + + + + editor.chain().focus().toggleBulletList().run()} + selected={editor.isActive("bulletList")} + > + + + + + editor.chain().focus().toggleOrderedList().run()} + selected={editor.isActive("orderedList")} + > + + + + + editor.chain().focus().setHorizontalRule().run()} + > + + + + + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx index 33e41628..022bf922 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx @@ -1,5 +1,5 @@ import { StoredRules, StoredStat } from "types/HomebrewCollection.type"; -import { useForm, SubmitHandler } from "react-hook-form"; +import { useForm, SubmitHandler, Controller } from "react-hook-form"; import { Button, Dialog, @@ -14,6 +14,7 @@ import { useEffect, useState } from "react"; import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; import { Preview } from "../../Preview"; import { StatPreviewComponent } from "./StatPreviewComponent"; +import { MarkdownEditor } from "components/shared/RichTextEditor/MarkdownEditor"; export interface StatDialogProps { stats: StoredRules["stats"]; @@ -107,22 +108,18 @@ export function StatDialog(props: StatDialogProps) { }), }} /> - ( + + )} /> diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx index 2b5dec88..a1db05e2 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx @@ -89,10 +89,7 @@ export function Stats(props: StatsProps) { } > - + ))} From 7815231beb19eab511c0c0ed095e3c692a0a2359 Mon Sep 17 00:00:00 2001 From: Scott Benton Date: Mon, 18 Dec 2023 17:45:05 -0500 Subject: [PATCH 8/9] feat(rules): Added impacts --- .../ConditionMeterAutocomplete.tsx | 37 +++ .../ConditionMeters/ConditionMeterDialog.tsx | 18 +- .../ConditionMeters/ConditionMeters.tsx | 35 +-- .../Impacts/ImpactCategoryDialog.tsx | 149 +++++++++++ .../RulesSection/Impacts/ImpactDialog.tsx | 215 ++++++++++++++++ .../RulesSection/Impacts/Impacts.tsx | 235 +++++++++++++++++- .../RulesSection/RulesSection.tsx | 17 +- .../SpecialTracks/SpecialTracks.tsx | 13 + .../RulesSection/SpecialTracks/index.ts | 1 + .../RulesSection/Stats/StatAutocomplete.tsx | 37 +++ .../RulesSection/Stats/StatDialog.tsx | 1 - .../RulesSection/Stats/Stats.tsx | 33 +-- src/types/HomebrewCollection.type.ts | 20 +- 13 files changed, 756 insertions(+), 55 deletions(-) create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterAutocomplete.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/ImpactCategoryDialog.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/ImpactDialog.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/SpecialTracks.tsx create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/index.ts create mode 100644 src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatAutocomplete.tsx diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterAutocomplete.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterAutocomplete.tsx new file mode 100644 index 00000000..19f73c95 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterAutocomplete.tsx @@ -0,0 +1,37 @@ +import { Autocomplete, TextField, capitalize } from "@mui/material"; +import { useRules } from "data/hooks/useRules"; +import { StoredConditionMeter } from "types/HomebrewCollection.type"; + +export interface StatAutocompleteProps { + conditionMeters: Record; + label?: string; + value: string[]; + onChange: (value: string[]) => void; + disabled?: boolean; + onBlur: () => void; +} + +export function ConditionMeterAutocomplete(props: StatAutocompleteProps) { + const { label, conditionMeters, value, onChange, disabled, onBlur } = props; + const { condition_meters: baseConditionMeters } = useRules(); + + const allConditionMeters = { ...conditionMeters, ...baseConditionMeters }; + + return ( + capitalize(allConditionMeters[key].label)} + renderInput={(params) => ( + + )} + value={value} + onChange={(evt, value) => { + const ids = Array.isArray(value) ? value : []; + onChange(ids); + }} + onBlur={onBlur} + disabled={disabled} + /> + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx index 1d573ed9..4d36604c 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/ConditionMeters/ConditionMeterDialog.tsx @@ -101,7 +101,6 @@ export function ConditionMeterDialog(props: ConditionMeterDialogProps) { if (!editingConditionMeterKey && value) { try { const id = convertIdPart(value); - console.debug(id); if (allConditionMeters[id]) { return `You already have a condition meter with id ${id}. Please try a different label.`; } @@ -113,6 +112,23 @@ export function ConditionMeterDialog(props: ConditionMeterDialogProps) { }), }} /> + - {Object.keys(conditionMeters).map((conditionMeterKey) => ( - + {Object.keys(conditionMeters) + .sort((c1, c2) => + conditionMeters[c1].label.localeCompare(conditionMeters[c2].label) + ) + .map((conditionMeterKey) => ( + + + { setConditionMeterDialogOpen(true); @@ -99,15 +110,9 @@ export function ConditionMeters(props: ConditionMetersProps) { handleDelete(conditionMeterKey)}> - - } - > - - - ))} + + + ))} )} + + + + + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/ImpactDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/ImpactDialog.tsx new file mode 100644 index 00000000..3fe3ac71 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/ImpactDialog.tsx @@ -0,0 +1,215 @@ +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + FormControl, + FormControlLabel, + FormHelperText, + Stack, + TextField, +} from "@mui/material"; +import { DialogTitleWithCloseButton } from "components/shared/DialogTitleWithCloseButton"; +import { convertIdPart } from "functions/dataswornIdEncoder"; +import { useEffect, useState } from "react"; +import { Controller, SubmitHandler, useForm } from "react-hook-form"; +import { + StoredConditionMeter, + StoredImpact, + StoredImpactCategory, +} from "types/HomebrewCollection.type"; +import { ConditionMeterAutocomplete } from "../ConditionMeters/ConditionMeterAutocomplete"; + +export interface ImpactDialogProps { + open: boolean; + onClose: () => void; + onSave: ( + impactCategoryId: string, + impactId: string, + impact: StoredImpact + ) => Promise; + impacts: StoredImpactCategory["contents"]; + editingCategoryKey: string; + editingImpactKey?: string; + conditionMeters: Record; +} + +export function ImpactDialog(props: ImpactDialogProps) { + const { + open, + onClose, + onSave, + impacts, + editingCategoryKey, + editingImpactKey, + conditionMeters, + } = props; + + const existingImpact = + editingCategoryKey && editingImpactKey + ? impacts[editingImpactKey] ?? undefined + : undefined; + + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + formState: { errors, touchedFields, disabled }, + reset, + control, + } = useForm({ disabled: loading }); + + useEffect(() => { + if (open) { + reset(existingImpact); + } + }, [open, reset, existingImpact]); + + const onSubmit: SubmitHandler = (values) => { + setLoading(true); + const id = editingImpactKey ?? convertIdPart(values.label); + onSave(editingCategoryKey, id, values) + .then(() => { + setLoading(false); + onClose(); + }) + .catch(() => { + setLoading(false); + }); + }; + + return ( + + + {editingImpactKey ? "Edit Impact" : "Add Impact"} + +
+ + + { + if (!editingImpactKey && value) { + try { + const id = convertIdPart(value); + if (impacts[id]) { + return `You already have an impact with id ${id}. Please try a different label.`; + } + } catch (e) { + return "Failed to parse a valid ID for your impact. Please use at least three letters or numbers in your label."; + } + } + }, + }), + }} + /> + + + ( + + )} + /> + } + label={"Permanent Impact?"} + /> + {touchedFields.permanent && errors.permanent && ( + {errors.permanent.message} + )} + + ( + field.onChange(ids)} + onBlur={field.onBlur} + /> + )} + /> + + ( + + )} + /> + } + label={"Shared across all players?"} + /> + {touchedFields.shared && errors.shared && ( + {errors.shared.message} + )} + + + + + + + +
+
+ ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/Impacts.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/Impacts.tsx index 043a3637..cf506dc4 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/Impacts.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Impacts/Impacts.tsx @@ -1,19 +1,104 @@ -import { Box, Button, List, Typography } from "@mui/material"; +import { + Box, + Button, + IconButton, + List, + ListItem, + ListItemText, + Typography, +} from "@mui/material"; import { useState } from "react"; -import { StoredRules } from "types/HomebrewCollection.type"; +import { + StoredImpact, + StoredImpactCategory, + StoredRules, +} from "types/HomebrewCollection.type"; +import { ImpactCategoryDialog } from "./ImpactCategoryDialog"; +import { useStore } from "stores/store"; +import EditIcon from "@mui/icons-material/Edit"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { useConfirm } from "material-ui-confirm"; +import { deleteField } from "firebase/firestore"; +import { ImpactDialog } from "./ImpactDialog"; export interface ImpactsProps { homebrewId: string; impactCategories: StoredRules["impacts"]; + conditionMeters: StoredRules["condition_meters"]; } export function Impacts(props: ImpactsProps) { - const { homebrewId, impactCategories } = props; + const { homebrewId, impactCategories, conditionMeters } = props; + const confirm = useConfirm(); + const [impactCategoryDialogOpen, setImpactCategoryDialogOpen] = + useState(false); const [impactDialogOpen, setImpactDialogOpen] = useState(false); const [editingImpactCategoryKey, setEditingImpactCategoryKey] = useState< string | undefined >(undefined); + const [editingImpactKey, setEditingImpactKey] = useState( + undefined + ); + + const updateRules = useStore((store) => store.homebrew.updateExpansionRules); + const addImpactCategory = ( + impactCategoryId: string, + impactCategory: StoredImpactCategory + ) => { + return updateRules(homebrewId, { + impacts: { [impactCategoryId]: impactCategory }, + }); + }; + const deleteImpactCategory = (categoryId: string) => { + return updateRules(homebrewId, { + impacts: { [categoryId]: deleteField() }, + }); + }; + const addImpact = ( + impactCategoryId: string, + impactId: string, + impact: StoredImpact + ) => { + return updateRules(homebrewId, { + impacts: { [impactCategoryId]: { contents: { [impactId]: impact } } }, + }); + }; + const deleteImpact = (categoryId: string, impactId: string) => { + return updateRules(homebrewId, { + impacts: { [categoryId]: { contents: { [impactId]: deleteField() } } }, + }); + }; + const handleCategoryDelete = (categoryId: string) => { + confirm({ + title: `Delete ${impactCategories[categoryId].label}`, + description: "Are you sure you want to delete this impact category?", + confirmationText: "Delete", + confirmationButtonProps: { + variant: "contained", + color: "error", + }, + }) + .then(() => { + deleteImpactCategory(categoryId).catch(() => {}); + }) + .catch(() => {}); + }; + const handleImpactDelete = (categoryId: string, impactId: string) => { + confirm({ + title: `Delete ${impactCategories[categoryId].contents[impactId].label}`, + description: "Are you sure you want to delete this impact?", + confirmationText: "Delete", + confirmationButtonProps: { + variant: "contained", + color: "error", + }, + }) + .then(() => { + deleteImpact(categoryId, impactId).catch(() => {}); + }) + .catch(() => {}); + }; return ( <> @@ -31,28 +116,160 @@ export function Impacts(props: ImpactsProps) { sm: "repeat(2, 1fr)", md: "repeat(3, 1fr)", }, + gap: 2, pl: 0, my: 0, listStyle: "none", }} > - {Object.keys(impactCategories).map((categoryKey) => ( - - {impactCategories[categoryKey].label} - - ))} + {Object.keys(impactCategories) + .sort((c1, c2) => + impactCategories[c1].label.localeCompare( + impactCategories[c2].label + ) + ) + .map((categoryKey) => ( + + + + + { + setImpactCategoryDialogOpen(true); + setEditingImpactCategoryKey(categoryKey); + }} + > + + + handleCategoryDelete(categoryKey)} + > + + + + + {Object.keys(impactCategories[categoryKey].contents).length > + 0 ? ( + + {Object.keys(impactCategories[categoryKey].contents) + .sort((i1, i2) => + impactCategories[categoryKey].contents[ + i1 + ].label.localeCompare( + impactCategories[categoryKey].contents[i2].label + ) + ) + .map((categoryContentKey) => ( + + + + { + setImpactDialogOpen(true); + setEditingImpactCategoryKey(categoryKey); + setEditingImpactKey(categoryContentKey); + }} + > + + + + handleImpactDelete( + categoryKey, + categoryContentKey + ) + } + > + + + + + ))} + + ) : ( + + No Impacts in this Category + + )} + + + ))} )} + setImpactCategoryDialogOpen(false)} + impactCategories={impactCategories} + onSave={addImpactCategory} + editingCategoryKey={editingImpactCategoryKey} + /> + {editingImpactCategoryKey && ( + setImpactDialogOpen(false)} + impacts={impactCategories[editingImpactCategoryKey]?.contents} + onSave={addImpact} + editingCategoryKey={editingImpactCategoryKey} + editingImpactKey={editingImpactKey} + conditionMeters={conditionMeters} + /> + )} ); } diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx index 600325af..afed62f7 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/RulesSection.tsx @@ -4,6 +4,7 @@ import { useStore } from "stores/store"; import { Stats } from "./Stats"; import { ConditionMeters } from "./ConditionMeters"; import { Impacts } from "./Impacts"; +import { SpecialTracks } from "./SpecialTracks"; export interface RulesSectionProps { id: string; @@ -18,8 +19,9 @@ export function RulesSection(props: RulesSectionProps) { const rules = useStore((store) => store.homebrew.collections[id].rules?.data); const stats = rules?.stats ?? {}; - const conditonMeters = rules?.condition_meters ?? {}; + const conditionMeters = rules?.condition_meters ?? {}; const impacts = rules?.impacts ?? {}; + const specialTracks = rules?.special_tracks ?? {}; if (loading) { return ; @@ -36,10 +38,15 @@ export function RulesSection(props: RulesSectionProps) { - - - - + + + + + ); } diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/SpecialTracks.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/SpecialTracks.tsx new file mode 100644 index 00000000..7eb14093 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/SpecialTracks.tsx @@ -0,0 +1,13 @@ +import { StoredRules } from "types/HomebrewCollection.type"; + +export interface SpecialTracksProps { + homebrewId: string; + specialTracks: StoredRules["special_tracks"]; +} + +export function SpecialTracks(props: SpecialTracksProps) { + const { homebrewId, specialTracks } = props; + // TODO - Waiting on rsek's potential XP changes before implementation + console.debug(homebrewId, specialTracks); + return <>Special Tracks; +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/index.ts b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/index.ts new file mode 100644 index 00000000..d1993103 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/SpecialTracks/index.ts @@ -0,0 +1 @@ +export * from "./SpecialTracks"; diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatAutocomplete.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatAutocomplete.tsx new file mode 100644 index 00000000..818cf3d1 --- /dev/null +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatAutocomplete.tsx @@ -0,0 +1,37 @@ +import { Autocomplete, TextField, capitalize } from "@mui/material"; +import { useRules } from "data/hooks/useRules"; +import { StoredStat } from "types/HomebrewCollection.type"; + +export interface StatAutocompleteProps { + stats: Record; + label?: string; + value: string[]; + onChange: (value: string[]) => void; + disabled?: boolean; + onBlur: () => void; +} + +export function StatAutocomplete(props: StatAutocompleteProps) { + const { label, stats, value, onChange, disabled, onBlur } = props; + const { stats: baseStats } = useRules(); + + const allStats = { ...stats, ...baseStats }; + + return ( + capitalize(allStats[statKey].label)} + renderInput={(params) => ( + + )} + value={value} + onChange={(evt, value) => { + const ids = Array.isArray(value) ? value : []; + onChange(ids); + }} + onBlur={onBlur} + disabled={disabled} + /> + ); +} diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx index 33e41628..fd437aa4 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/StatDialog.tsx @@ -95,7 +95,6 @@ export function StatDialog(props: StatDialogProps) { if (!editingStatKey && value) { try { const id = convertIdPart(value); - console.debug(id); if (existingStatKeys.includes(id)) { return `You already have a stat with id ${id}. Please try a different label.`; } diff --git a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx index 2b5dec88..a23b1c95 100644 --- a/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx +++ b/src/pages/Homebrew/HomebrewEditorPage/RulesSection/Stats/Stats.tsx @@ -1,4 +1,5 @@ import { + Box, Button, IconButton, List, @@ -69,12 +70,20 @@ export function Stats(props: StatsProps) { listStyle: "none", }} > - {Object.keys(stats).map((statKey) => ( - + {Object.keys(stats) + .sort((s1, s2) => stats[s1].label.localeCompare(stats[s2].label)) + .map((statKey) => ( + + + { setStatDialogOpen(true); @@ -86,15 +95,9 @@ export function Stats(props: StatsProps) { handleDelete(statKey)}> - - } - > - - - ))} + + + ))} )}