From 387068014b76b0b4511754c426e59e182d4267ac Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 13:12:38 +0000 Subject: [PATCH 01/64] Add order outcomes --- src/Router.tsx | 2 +- .../GameDetailsLayout/GameDetailsLayout.tsx | 3 +- src/components/Map/Map.tsx | 2 +- src/components/Orders/Orders.tsx | 78 +++++++++++++++---- src/components/Orders/index.ts | 2 +- .../{Orders.utils.ts => orders-utils.ts} | 10 ++- .../Orders/{Orders.hook.ts => use-orders.ts} | 8 +- src/theme.ts | 10 +++ 8 files changed, 90 insertions(+), 25 deletions(-) rename src/components/Orders/{Orders.utils.ts => orders-utils.ts} (66%) rename src/components/Orders/{Orders.hook.ts => use-orders.ts} (78%) diff --git a/src/Router.tsx b/src/Router.tsx index 735c4aa9..01226da6 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -8,7 +8,7 @@ import UserPage from "./screens/UserPage"; import { Map } from "./components/Map"; import { GameDetailsLayout } from "./components/GameDetailsLayout"; import { GameDetailsNavigation } from "./components/GameDetailsNavigation"; -import { Orders } from "./components/Orders"; +import { Orders } from "./components/orders"; import { CreateOrder } from "./components/CreateOrder"; import { Modal } from "./components/Modal"; import { CreateOrderAction } from "./components/CreateOrderAction"; diff --git a/src/components/GameDetailsLayout/GameDetailsLayout.tsx b/src/components/GameDetailsLayout/GameDetailsLayout.tsx index e51a0020..6270c3d1 100644 --- a/src/components/GameDetailsLayout/GameDetailsLayout.tsx +++ b/src/components/GameDetailsLayout/GameDetailsLayout.tsx @@ -20,8 +20,9 @@ const GameDetailsLayout: React.FC<{ diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 2ed0e609..886cb66f 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -35,7 +35,7 @@ const Map: React.FC = () => {
); diff --git a/src/components/Orders/Orders.tsx b/src/components/Orders/Orders.tsx index 0cc319b6..c8b05d50 100644 --- a/src/components/Orders/Orders.tsx +++ b/src/components/Orders/Orders.tsx @@ -3,13 +3,22 @@ import { CircularProgress, List, ListItem, + ListItemIcon, ListItemText, ListSubheader, Stack, + styled, + Theme, Typography, + useTheme, } from "@mui/material"; -import { useOrders } from "./Orders.hook"; +import { + CheckCircle as OrderOkIcon, + Cancel as OrderErrorIcon, +} from "@mui/icons-material"; + +import { useOrders } from "./use-orders"; type OrderProps = { source: string; @@ -18,6 +27,17 @@ type OrderProps = { aux?: string; }; +const StyledListItem = styled(ListItem)(({ theme }) => ({ + border: `1px solid ${theme.palette.grey[200]}`, +})); + +const StyledListSubheader = styled(ListSubheader)(({ theme }) => ({ + textAlign: "left", + fontWeight: "bold", + border: `1px solid ${theme.palette.grey[200]}`, + backgroundColor: theme.palette.grey[200], +})); + const formatOrderText = (order: OrderProps) => { if (order.orderType === "Hold") { return `${order.source} Hold`; @@ -28,7 +48,26 @@ const formatOrderText = (order: OrderProps) => { return `${order.source} ${order.orderType} to ${order.target}`; }; +const OutcomeLabelMap = { + OK: "Succeeded", + Bounced: "Bounced", + SupportBroken: "Support broken", +} as const; + +const OutcomeIconMap = { + OK: (theme: Theme) => ( + + ), + Bounced: (theme: Theme) => ( + + ), + SupportBroken: (theme: Theme) => ( + + ), +} as const; + const Orders: React.FC = () => { + const theme = useTheme(); const { isLoading, isError, orders } = useOrders(); if (isLoading) { @@ -56,22 +95,29 @@ const Orders: React.FC = () => { Orders {orders.map(({ nation, orders }) => ( - - {nation} - - } - > + {nation}}> {orders.map((order, index) => ( - - {formatOrderText(order)} - + + {order.outcome && ( + + {OutcomeIconMap[order.outcome](theme)} + + )} + + + + {formatOrderText(order)} + + + {order.outcome && ( + + + {OutcomeLabelMap[order.outcome]} + + + )} + + ))} diff --git a/src/components/Orders/index.ts b/src/components/Orders/index.ts index 5d3677a0..9960c475 100644 --- a/src/components/Orders/index.ts +++ b/src/components/Orders/index.ts @@ -1 +1 @@ -export * from "./Orders"; \ No newline at end of file +export * from "./orders"; \ No newline at end of file diff --git a/src/components/Orders/Orders.utils.ts b/src/components/Orders/orders-utils.ts similarity index 66% rename from src/components/Orders/Orders.utils.ts rename to src/components/Orders/orders-utils.ts index 04d9193d..f0e50004 100644 --- a/src/components/Orders/Orders.utils.ts +++ b/src/components/Orders/orders-utils.ts @@ -1,14 +1,18 @@ -import { useGetOrdersQuery, useGetVariantQuery } from "../../common"; +import { useGetOrdersQuery, useGetPhaseQuery, useGetVariantQuery } from "../../common"; + +type Outcome = "OK" | "Bounced" | "SupportBroken" | undefined; type Order = { source: string; orderType: string; target: string | undefined; aux: string | undefined; + outcome: Outcome; }; const createOrders = ( variant: NonNullable["data"]>, + phase: NonNullable["data"]>, orders: NonNullable["data"]> ) => { const ordersByNation = new Map(); @@ -17,12 +21,16 @@ const createOrders = ( const [source, orderType, target, aux] = order.Parts; if (!source) throw new Error("No source found"); if (!orderType) throw new Error("No orderType found"); + const resolution = phase.Resolutions?.find((resolution) => resolution.Province === source); + + const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : undefined; const orderData = { source: variant.getProvinceLongName(source), orderType: orderType, target: target ? variant.getProvinceLongName(target) : undefined, aux: aux ? variant.getProvinceLongName(aux) : undefined, + outcome: outcome }; if (!ordersByNation.has(order.Nation)) { diff --git a/src/components/Orders/Orders.hook.ts b/src/components/Orders/use-orders.ts similarity index 78% rename from src/components/Orders/Orders.hook.ts rename to src/components/Orders/use-orders.ts index 12f11203..79af0012 100644 --- a/src/components/Orders/Orders.hook.ts +++ b/src/components/Orders/use-orders.ts @@ -1,5 +1,5 @@ -import { mergeQueries, useGetVariantQuery, useGetOrdersQuery } from "../../common"; -import { createOrders } from "./Orders.utils"; +import { mergeQueries, useGetVariantQuery, useGetOrdersQuery, useGetPhaseQuery } from "../../common"; +import { createOrders } from "./orders-utils"; import { useSelectedPhaseContext } from "../selected-phase-context"; import { useGameDetailContext } from "../game-detail-context"; @@ -23,13 +23,13 @@ type SuccessState = { const useOrders = (): LoadingState | ErrorState | SuccessState => { const { gameId } = useGameDetailContext(); - const { selectedPhase } = useSelectedPhaseContext(); const getVariantQuery = useGetVariantQuery(gameId); + const getPhaseQuery = useGetPhaseQuery(gameId, selectedPhase); const listOrdersQuery = useGetOrdersQuery(gameId, selectedPhase); - const { isLoading, isError, data } = mergeQueries([getVariantQuery, listOrdersQuery], createOrders); + const { isLoading, isError, data } = mergeQueries([getVariantQuery, getPhaseQuery, listOrdersQuery], createOrders); if (isLoading) return { isLoading: true }; if (isError) return { isError: true }; diff --git a/src/theme.ts b/src/theme.ts index 41d673bc..286a86ba 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -13,6 +13,16 @@ const theme = createTheme({ lineHeight: "26px", fontWeight: 600, }, + h4: { + fontSize: "16px", + lineHeight: "24px", + fontWeight: 600, + }, + caption: { + fontSize: "12px", + lineHeight: "18px", + color: "rgb(89, 99, 110)" + }, button: { textTransform: "none", } From b2d061c7541793ba85beb7d04bcad642774f847d Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 13:17:19 +0000 Subject: [PATCH 02/64] case --- src/components/orders/index.ts | 1 + src/components/orders/orders-utils.ts | 49 +++++++++++++++++++ .../{Orders/Orders.tsx => orders/orders.tsx} | 0 src/components/orders/use-orders.ts | 43 ++++++++++++++++ 4 files changed, 93 insertions(+) create mode 100644 src/components/orders/index.ts create mode 100644 src/components/orders/orders-utils.ts rename src/components/{Orders/Orders.tsx => orders/orders.tsx} (100%) create mode 100644 src/components/orders/use-orders.ts diff --git a/src/components/orders/index.ts b/src/components/orders/index.ts new file mode 100644 index 00000000..9960c475 --- /dev/null +++ b/src/components/orders/index.ts @@ -0,0 +1 @@ +export * from "./orders"; \ No newline at end of file diff --git a/src/components/orders/orders-utils.ts b/src/components/orders/orders-utils.ts new file mode 100644 index 00000000..f0e50004 --- /dev/null +++ b/src/components/orders/orders-utils.ts @@ -0,0 +1,49 @@ +import { useGetOrdersQuery, useGetPhaseQuery, useGetVariantQuery } from "../../common"; + +type Outcome = "OK" | "Bounced" | "SupportBroken" | undefined; + +type Order = { + source: string; + orderType: string; + target: string | undefined; + aux: string | undefined; + outcome: Outcome; +}; + +const createOrders = ( + variant: NonNullable["data"]>, + phase: NonNullable["data"]>, + orders: NonNullable["data"]> +) => { + const ordersByNation = new Map(); + + orders.Orders.forEach((order) => { + const [source, orderType, target, aux] = order.Parts; + if (!source) throw new Error("No source found"); + if (!orderType) throw new Error("No orderType found"); + const resolution = phase.Resolutions?.find((resolution) => resolution.Province === source); + + const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : undefined; + + const orderData = { + source: variant.getProvinceLongName(source), + orderType: orderType, + target: target ? variant.getProvinceLongName(target) : undefined, + aux: aux ? variant.getProvinceLongName(aux) : undefined, + outcome: outcome + }; + + if (!ordersByNation.has(order.Nation)) { + ordersByNation.set(order.Nation, []); + } + + ordersByNation.get(order.Nation)!.push(orderData); + }); + + return Array.from(ordersByNation.entries()).map(([nation, orders]) => ({ + nation, + orders, + })); +} + +export { createOrders }; \ No newline at end of file diff --git a/src/components/Orders/Orders.tsx b/src/components/orders/orders.tsx similarity index 100% rename from src/components/Orders/Orders.tsx rename to src/components/orders/orders.tsx diff --git a/src/components/orders/use-orders.ts b/src/components/orders/use-orders.ts new file mode 100644 index 00000000..79af0012 --- /dev/null +++ b/src/components/orders/use-orders.ts @@ -0,0 +1,43 @@ +import { mergeQueries, useGetVariantQuery, useGetOrdersQuery, useGetPhaseQuery } from "../../common"; +import { createOrders } from "./orders-utils"; +import { useSelectedPhaseContext } from "../selected-phase-context"; +import { useGameDetailContext } from "../game-detail-context"; + +type LoadingState = { + isLoading: true; + isError?: never; + orders?: never; +}; + +type ErrorState = { + isLoading?: never; + isError: true; + orders?: never; +}; + +type SuccessState = { + isLoading?: never; + isError?: never; + orders: ReturnType; +}; + +const useOrders = (): LoadingState | ErrorState | SuccessState => { + const { gameId } = useGameDetailContext(); + const { selectedPhase } = useSelectedPhaseContext(); + + const getVariantQuery = useGetVariantQuery(gameId); + const getPhaseQuery = useGetPhaseQuery(gameId, selectedPhase); + const listOrdersQuery = useGetOrdersQuery(gameId, selectedPhase); + + const { isLoading, isError, data } = mergeQueries([getVariantQuery, getPhaseQuery, listOrdersQuery], createOrders); + + if (isLoading) return { isLoading: true }; + if (isError) return { isError: true }; + if (!data) throw new Error("No data found"); + + return { + orders: data, + } +}; + +export { useOrders }; \ No newline at end of file From 0c11b3aa93e4f2af63d0337cece092617fd7cce9 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 13:22:42 +0000 Subject: [PATCH 03/64] Fix casing --- ...e-static-web-apps-blue-cliff-00777a403.yml | 92 +++++++++--------- ...re-static-web-apps-nice-sand-001bca703.yml | 94 +++++++++---------- src/components/Orders/index.ts | 1 - src/components/Orders/orders-utils.ts | 49 ---------- src/components/Orders/use-orders.ts | 43 --------- 5 files changed, 93 insertions(+), 186 deletions(-) delete mode 100644 src/components/Orders/index.ts delete mode 100644 src/components/Orders/orders-utils.ts delete mode 100644 src/components/Orders/use-orders.ts diff --git a/.github/workflows/azure-static-web-apps-blue-cliff-00777a403.yml b/.github/workflows/azure-static-web-apps-blue-cliff-00777a403.yml index f10a75fa..fb70f4ae 100644 --- a/.github/workflows/azure-static-web-apps-blue-cliff-00777a403.yml +++ b/.github/workflows/azure-static-web-apps-blue-cliff-00777a403.yml @@ -1,46 +1,46 @@ -name: Azure Static Web Apps CI/CD - -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BLUE_CLIFF_00777A403 }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/" # App source code path - api_location: "api" # Api source code path - optional - output_location: "/dist" # Built app content directory - optional - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BLUE_CLIFF_00777A403 }} - action: "close" +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + lfs: false + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BLUE_CLIFF_00777A403 }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "/" # App source code path + api_location: "api" # Api source code path - optional + output_location: "/dist" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_BLUE_CLIFF_00777A403 }} + action: "close" diff --git a/.github/workflows/azure-static-web-apps-nice-sand-001bca703.yml b/.github/workflows/azure-static-web-apps-nice-sand-001bca703.yml index bb47ba18..984b6068 100644 --- a/.github/workflows/azure-static-web-apps-nice-sand-001bca703.yml +++ b/.github/workflows/azure-static-web-apps-nice-sand-001bca703.yml @@ -1,47 +1,47 @@ -name: Azure Static Web Apps CI/CD - -on: - push: - branches: - - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main - -jobs: - build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') - runs-on: ubuntu-latest - name: Build and Deploy Job - steps: - - uses: actions/checkout@v3 - with: - submodules: true - lfs: false - - name: Build And Deploy - id: builddeploy - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_SAND_001BCA703 }} - repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) - action: "upload" - ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### - # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig - app_location: "/" # App source code path - app_build_command: "npm run build-storybook" - api_location: "api" # Api source code path - optional - output_location: "/storybook-static" # Built app content directory - optional - ###### End of Repository/Build Configurations ###### - - close_pull_request_job: - if: github.event_name == 'pull_request' && github.event.action == 'closed' - runs-on: ubuntu-latest - name: Close Pull Request Job - steps: - - name: Close Pull Request - id: closepullrequest - uses: Azure/static-web-apps-deploy@v1 - with: - azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_SAND_001BCA703 }} - action: "close" +name: Azure Static Web Apps CI/CD + +on: + push: + branches: + - main + pull_request: + types: [opened, synchronize, reopened, closed] + branches: + - main + +jobs: + build_and_deploy_job: + if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') + runs-on: ubuntu-latest + name: Build and Deploy Job + steps: + - uses: actions/checkout@v3 + with: + submodules: true + lfs: false + - name: Build And Deploy + id: builddeploy + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_SAND_001BCA703 }} + repo_token: ${{ secrets.GITHUB_TOKEN }} # Used for Github integrations (i.e. PR comments) + action: "upload" + ###### Repository/Build Configurations - These values can be configured to match your app requirements. ###### + # For more information regarding Static Web App workflow configurations, please visit: https://aka.ms/swaworkflowconfig + app_location: "/" # App source code path + app_build_command: "npm run build-storybook" + api_location: "api" # Api source code path - optional + output_location: "/storybook-static" # Built app content directory - optional + ###### End of Repository/Build Configurations ###### + + close_pull_request_job: + if: github.event_name == 'pull_request' && github.event.action == 'closed' + runs-on: ubuntu-latest + name: Close Pull Request Job + steps: + - name: Close Pull Request + id: closepullrequest + uses: Azure/static-web-apps-deploy@v1 + with: + azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN_NICE_SAND_001BCA703 }} + action: "close" diff --git a/src/components/Orders/index.ts b/src/components/Orders/index.ts deleted file mode 100644 index 9960c475..00000000 --- a/src/components/Orders/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./orders"; \ No newline at end of file diff --git a/src/components/Orders/orders-utils.ts b/src/components/Orders/orders-utils.ts deleted file mode 100644 index f0e50004..00000000 --- a/src/components/Orders/orders-utils.ts +++ /dev/null @@ -1,49 +0,0 @@ -import { useGetOrdersQuery, useGetPhaseQuery, useGetVariantQuery } from "../../common"; - -type Outcome = "OK" | "Bounced" | "SupportBroken" | undefined; - -type Order = { - source: string; - orderType: string; - target: string | undefined; - aux: string | undefined; - outcome: Outcome; -}; - -const createOrders = ( - variant: NonNullable["data"]>, - phase: NonNullable["data"]>, - orders: NonNullable["data"]> -) => { - const ordersByNation = new Map(); - - orders.Orders.forEach((order) => { - const [source, orderType, target, aux] = order.Parts; - if (!source) throw new Error("No source found"); - if (!orderType) throw new Error("No orderType found"); - const resolution = phase.Resolutions?.find((resolution) => resolution.Province === source); - - const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : undefined; - - const orderData = { - source: variant.getProvinceLongName(source), - orderType: orderType, - target: target ? variant.getProvinceLongName(target) : undefined, - aux: aux ? variant.getProvinceLongName(aux) : undefined, - outcome: outcome - }; - - if (!ordersByNation.has(order.Nation)) { - ordersByNation.set(order.Nation, []); - } - - ordersByNation.get(order.Nation)!.push(orderData); - }); - - return Array.from(ordersByNation.entries()).map(([nation, orders]) => ({ - nation, - orders, - })); -} - -export { createOrders }; \ No newline at end of file diff --git a/src/components/Orders/use-orders.ts b/src/components/Orders/use-orders.ts deleted file mode 100644 index 79af0012..00000000 --- a/src/components/Orders/use-orders.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { mergeQueries, useGetVariantQuery, useGetOrdersQuery, useGetPhaseQuery } from "../../common"; -import { createOrders } from "./orders-utils"; -import { useSelectedPhaseContext } from "../selected-phase-context"; -import { useGameDetailContext } from "../game-detail-context"; - -type LoadingState = { - isLoading: true; - isError?: never; - orders?: never; -}; - -type ErrorState = { - isLoading?: never; - isError: true; - orders?: never; -}; - -type SuccessState = { - isLoading?: never; - isError?: never; - orders: ReturnType; -}; - -const useOrders = (): LoadingState | ErrorState | SuccessState => { - const { gameId } = useGameDetailContext(); - const { selectedPhase } = useSelectedPhaseContext(); - - const getVariantQuery = useGetVariantQuery(gameId); - const getPhaseQuery = useGetPhaseQuery(gameId, selectedPhase); - const listOrdersQuery = useGetOrdersQuery(gameId, selectedPhase); - - const { isLoading, isError, data } = mergeQueries([getVariantQuery, getPhaseQuery, listOrdersQuery], createOrders); - - if (isLoading) return { isLoading: true }; - if (isError) return { isError: true }; - if (!data) throw new Error("No data found"); - - return { - orders: data, - } -}; - -export { useOrders }; \ No newline at end of file From 274de07dfee65b6a11e693e82925a8d1ae1a3eac Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 17:59:42 +0000 Subject: [PATCH 04/64] Move contexts to context sub-folder --- .gitignore | 2 - .vscode/tasks.json | 45 +++++++++++++++++++ .../ConfirmOrdersAction.tsx | 2 +- .../CreateOrder/CreateOrder.hook.ts | 2 +- .../CreateOrderAction.hook.ts | 2 +- .../GameDetailsLayout/GameDetailsLayout.tsx | 6 ++- .../GameDetailsNavigation.tsx | 2 +- src/components/Map/use-map.ts | 3 +- src/components/orders/use-orders.ts | 3 +- .../phase-select/use-phase-select.ts | 3 +- .../game-detail-context-provider.tsx | 0 .../game-detail-context.ts | 0 .../game-detail.context.types.ts | 0 .../game-detail-context/index.ts | 0 .../use-game-detail-context.ts | 0 src/context/index.ts | 2 + .../selected-phase-context/index.ts | 0 .../selected-phase-context-provider.tsx | 0 .../selected-phase-context.ts | 0 .../selected-phase-context.types.ts | 0 .../use-selected-phase-context.ts | 0 21 files changed, 58 insertions(+), 14 deletions(-) create mode 100644 .vscode/tasks.json rename src/{components => context}/game-detail-context/game-detail-context-provider.tsx (100%) rename src/{components => context}/game-detail-context/game-detail-context.ts (100%) rename src/{components => context}/game-detail-context/game-detail.context.types.ts (100%) rename src/{components => context}/game-detail-context/index.ts (100%) rename src/{components => context}/game-detail-context/use-game-detail-context.ts (100%) create mode 100644 src/context/index.ts rename src/{components => context}/selected-phase-context/index.ts (100%) rename src/{components => context}/selected-phase-context/selected-phase-context-provider.tsx (100%) rename src/{components => context}/selected-phase-context/selected-phase-context.ts (100%) rename src/{components => context}/selected-phase-context/selected-phase-context.types.ts (100%) rename src/{components => context}/selected-phase-context/use-selected-phase-context.ts (100%) diff --git a/.gitignore b/.gitignore index 7b74e43c..013166ed 100644 --- a/.gitignore +++ b/.gitignore @@ -13,8 +13,6 @@ dist-ssr *.local # Editor directories and files -.vscode/* -!.vscode/extensions.json .idea .DS_Store *.suo diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 00000000..3f9d469d --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,45 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "dev", + "type": "shell", + "command": "npm run dev", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true, + "close": false + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "runOptions": { + "runOn": "folderOpen" + } + }, + { + "label": "build:watch", + "type": "shell", + "command": "npm run build-watch", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true, + "close": false + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "runOptions": { + "runOn": "folderOpen" + } + } + ] +} diff --git a/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx b/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx index 6e3750d2..d2aeeb50 100644 --- a/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx +++ b/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx @@ -8,7 +8,7 @@ import { useGetUserNewestPhaseStateQuery, useUpdatePhaseStateMutation, } from "../../common"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext } from "../../context"; const useConfirmOrdersAction = () => { const { gameId } = useGameDetailContext(); diff --git a/src/components/CreateOrder/CreateOrder.hook.ts b/src/components/CreateOrder/CreateOrder.hook.ts index fb04852c..668d1f92 100644 --- a/src/components/CreateOrder/CreateOrder.hook.ts +++ b/src/components/CreateOrder/CreateOrder.hook.ts @@ -2,7 +2,7 @@ import { useState } from "react"; import { mergeQueries, useCreateOrderMutation, useGetOptionsQuery, useGetVariantQuery } from "../../common"; import { createOrderOptionTree, getNextOptionsNode, getOrderStatus } from "./CreateOrder.util"; import { useModal } from "../Modal"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext } from "../../context"; type LoadingState = { isLoading: true; diff --git a/src/components/CreateOrderAction/CreateOrderAction.hook.ts b/src/components/CreateOrderAction/CreateOrderAction.hook.ts index 9c2d9a0d..102ae35d 100644 --- a/src/components/CreateOrderAction/CreateOrderAction.hook.ts +++ b/src/components/CreateOrderAction/CreateOrderAction.hook.ts @@ -1,6 +1,6 @@ import { useLocation, useNavigate } from "react-router"; import { mergeQueries, useGetCurrentPhaseQuery } from "../../common"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext } from "../../context"; const useCreateOrderAction = () => { const { gameId } = useGameDetailContext(); diff --git a/src/components/GameDetailsLayout/GameDetailsLayout.tsx b/src/components/GameDetailsLayout/GameDetailsLayout.tsx index 6270c3d1..8354ccca 100644 --- a/src/components/GameDetailsLayout/GameDetailsLayout.tsx +++ b/src/components/GameDetailsLayout/GameDetailsLayout.tsx @@ -2,8 +2,10 @@ import { Outlet } from "react-router"; import { AppBar, IconButton, Stack, Toolbar, useTheme } from "@mui/material"; import { ArrowBack as BackIcon } from "@mui/icons-material"; import { PhaseSelect } from "../phase-select"; -import { SelectedPhaseContextProvider } from "../selected-phase-context"; -import { GameDetailContextProvider } from "../game-detail-context"; +import { + GameDetailContextProvider, + SelectedPhaseContextProvider, +} from "../../context"; const GameDetailsLayout: React.FC<{ onClickBack: () => void; diff --git a/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx b/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx index 55ebfc7f..e48161d1 100644 --- a/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx +++ b/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx @@ -9,7 +9,7 @@ import { People as PlayersIcon, } from "@mui/icons-material"; import { useLocation, useNavigate } from "react-router"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext } from "../../context"; const GameDetailsNavigation: React.FC = () => { const { gameId } = useGameDetailContext(); diff --git a/src/components/Map/use-map.ts b/src/components/Map/use-map.ts index 5cd713b9..55b7956d 100644 --- a/src/components/Map/use-map.ts +++ b/src/components/Map/use-map.ts @@ -1,7 +1,6 @@ import { createMap } from "../../common/map/map"; import { mergeQueries, useGetPhaseQuery, useGetMapSvgQuery, useGetUnitSvgQuery, useGetVariantQuery } from "../../common"; -import { useSelectedPhaseContext } from "../selected-phase-context"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext, useSelectedPhaseContext } from "../../context"; const useMap = () => { const { gameId } = useGameDetailContext(); diff --git a/src/components/orders/use-orders.ts b/src/components/orders/use-orders.ts index 79af0012..c746b838 100644 --- a/src/components/orders/use-orders.ts +++ b/src/components/orders/use-orders.ts @@ -1,7 +1,6 @@ import { mergeQueries, useGetVariantQuery, useGetOrdersQuery, useGetPhaseQuery } from "../../common"; import { createOrders } from "./orders-utils"; -import { useSelectedPhaseContext } from "../selected-phase-context"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext, useSelectedPhaseContext } from "../../context"; type LoadingState = { isLoading: true; diff --git a/src/components/phase-select/use-phase-select.ts b/src/components/phase-select/use-phase-select.ts index 20b90134..2b1acade 100644 --- a/src/components/phase-select/use-phase-select.ts +++ b/src/components/phase-select/use-phase-select.ts @@ -1,7 +1,6 @@ import service from "../../common/store/service"; import { mergeQueries } from "../../common"; -import { useSelectedPhaseContext } from "../selected-phase-context"; -import { useGameDetailContext } from "../game-detail-context"; +import { useGameDetailContext, useSelectedPhaseContext } from "../../context"; const usePhaseSelect = () => { const { gameId } = useGameDetailContext(); diff --git a/src/components/game-detail-context/game-detail-context-provider.tsx b/src/context/game-detail-context/game-detail-context-provider.tsx similarity index 100% rename from src/components/game-detail-context/game-detail-context-provider.tsx rename to src/context/game-detail-context/game-detail-context-provider.tsx diff --git a/src/components/game-detail-context/game-detail-context.ts b/src/context/game-detail-context/game-detail-context.ts similarity index 100% rename from src/components/game-detail-context/game-detail-context.ts rename to src/context/game-detail-context/game-detail-context.ts diff --git a/src/components/game-detail-context/game-detail.context.types.ts b/src/context/game-detail-context/game-detail.context.types.ts similarity index 100% rename from src/components/game-detail-context/game-detail.context.types.ts rename to src/context/game-detail-context/game-detail.context.types.ts diff --git a/src/components/game-detail-context/index.ts b/src/context/game-detail-context/index.ts similarity index 100% rename from src/components/game-detail-context/index.ts rename to src/context/game-detail-context/index.ts diff --git a/src/components/game-detail-context/use-game-detail-context.ts b/src/context/game-detail-context/use-game-detail-context.ts similarity index 100% rename from src/components/game-detail-context/use-game-detail-context.ts rename to src/context/game-detail-context/use-game-detail-context.ts diff --git a/src/context/index.ts b/src/context/index.ts new file mode 100644 index 00000000..1a273fd9 --- /dev/null +++ b/src/context/index.ts @@ -0,0 +1,2 @@ +export * from "./game-detail-context"; +export * from "./selected-phase-context"; \ No newline at end of file diff --git a/src/components/selected-phase-context/index.ts b/src/context/selected-phase-context/index.ts similarity index 100% rename from src/components/selected-phase-context/index.ts rename to src/context/selected-phase-context/index.ts diff --git a/src/components/selected-phase-context/selected-phase-context-provider.tsx b/src/context/selected-phase-context/selected-phase-context-provider.tsx similarity index 100% rename from src/components/selected-phase-context/selected-phase-context-provider.tsx rename to src/context/selected-phase-context/selected-phase-context-provider.tsx diff --git a/src/components/selected-phase-context/selected-phase-context.ts b/src/context/selected-phase-context/selected-phase-context.ts similarity index 100% rename from src/components/selected-phase-context/selected-phase-context.ts rename to src/context/selected-phase-context/selected-phase-context.ts diff --git a/src/components/selected-phase-context/selected-phase-context.types.ts b/src/context/selected-phase-context/selected-phase-context.types.ts similarity index 100% rename from src/components/selected-phase-context/selected-phase-context.types.ts rename to src/context/selected-phase-context/selected-phase-context.types.ts diff --git a/src/components/selected-phase-context/use-selected-phase-context.ts b/src/context/selected-phase-context/use-selected-phase-context.ts similarity index 100% rename from src/components/selected-phase-context/use-selected-phase-context.ts rename to src/context/selected-phase-context/use-selected-phase-context.ts From 36b6b78c419faf91a0fc8c700bac83ce79b07cfb Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 18:02:59 +0000 Subject: [PATCH 05/64] Refactor IconButton usage in PhaseSelect component to use StyledIconButton for consistent styling --- src/components/phase-select/phase-select.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/components/phase-select/phase-select.tsx b/src/components/phase-select/phase-select.tsx index 682de16c..ec375b19 100644 --- a/src/components/phase-select/phase-select.tsx +++ b/src/components/phase-select/phase-select.tsx @@ -29,6 +29,10 @@ const StyledFormControl = styled(FormControl)` min-width: 250px; `; +const StyledIconButton = styled(IconButton)` + height: fit-content; +`; + const PhaseSelect = () => { const { handleNext, @@ -44,8 +48,7 @@ const PhaseSelect = () => { return ( - { disabled={previousDisabled} > - + { ))} - { disabled={nextDisabled} > - + ); }; From 400fa47c88c8e6074f1f4b663aed994f5b11b290 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 18:06:25 +0000 Subject: [PATCH 06/64] Add "Invalid" outcome to orders utility and update label and icon mappings --- src/components/orders/orders-utils.ts | 4 ++-- src/components/orders/orders.tsx | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/orders/orders-utils.ts b/src/components/orders/orders-utils.ts index f0e50004..f869c149 100644 --- a/src/components/orders/orders-utils.ts +++ b/src/components/orders/orders-utils.ts @@ -1,6 +1,6 @@ import { useGetOrdersQuery, useGetPhaseQuery, useGetVariantQuery } from "../../common"; -type Outcome = "OK" | "Bounced" | "SupportBroken" | undefined; +type Outcome = "OK" | "Bounced" | "SupportBroken" | "Invalid" | undefined; type Order = { source: string; @@ -23,7 +23,7 @@ const createOrders = ( if (!orderType) throw new Error("No orderType found"); const resolution = phase.Resolutions?.find((resolution) => resolution.Province === source); - const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : undefined; + const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : resolution?.Resolution.includes("Invalid") ? "Invalid" : undefined; const orderData = { source: variant.getProvinceLongName(source), diff --git a/src/components/orders/orders.tsx b/src/components/orders/orders.tsx index c8b05d50..44488d65 100644 --- a/src/components/orders/orders.tsx +++ b/src/components/orders/orders.tsx @@ -52,6 +52,7 @@ const OutcomeLabelMap = { OK: "Succeeded", Bounced: "Bounced", SupportBroken: "Support broken", + Invalid: "Invalid order", } as const; const OutcomeIconMap = { @@ -64,6 +65,9 @@ const OutcomeIconMap = { SupportBroken: (theme: Theme) => ( ), + Invalid: (theme: Theme) => ( + + ), } as const; const Orders: React.FC = () => { From 20450516c7d30c7dd653448b762f026d5fed59b9 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 18:09:10 +0000 Subject: [PATCH 07/64] wip --- src/components/orders/orders.tsx | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/orders/orders.tsx b/src/components/orders/orders.tsx index 44488d65..4f017221 100644 --- a/src/components/orders/orders.tsx +++ b/src/components/orders/orders.tsx @@ -8,7 +8,6 @@ import { ListSubheader, Stack, styled, - Theme, Typography, useTheme, } from "@mui/material"; @@ -55,21 +54,6 @@ const OutcomeLabelMap = { Invalid: "Invalid order", } as const; -const OutcomeIconMap = { - OK: (theme: Theme) => ( - - ), - Bounced: (theme: Theme) => ( - - ), - SupportBroken: (theme: Theme) => ( - - ), - Invalid: (theme: Theme) => ( - - ), -} as const; - const Orders: React.FC = () => { const theme = useTheme(); const { isLoading, isError, orders } = useOrders(); @@ -104,7 +88,13 @@ const Orders: React.FC = () => { {order.outcome && ( - {OutcomeIconMap[order.outcome](theme)} + {order.outcome === "OK" ? ( + + ) : ( + + )} )} From 066b362f9a2b01c99c687275a87d0310b6e02612 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 10 Jan 2025 18:50:41 +0000 Subject: [PATCH 08/64] Add utility functions for order formatting and resolution transformation; implement tests and update order handling --- src/__tests__/util.test.ts | 43 ++++++++++ src/common/schema/list-phases.ts | 10 ++- src/components/orders/orders-utils.ts | 16 ++-- src/components/orders/orders.tsx | 111 ++++++++++---------------- src/util.ts | 41 ++++++++++ 5 files changed, 143 insertions(+), 78 deletions(-) create mode 100644 src/__tests__/util.test.ts create mode 100644 src/util.ts diff --git a/src/__tests__/util.test.ts b/src/__tests__/util.test.ts new file mode 100644 index 00000000..01cb20c5 --- /dev/null +++ b/src/__tests__/util.test.ts @@ -0,0 +1,43 @@ +import { describe, test } from "vitest"; +import { transformResolution } from "../util"; + +describe("transformResolution", () => { + test("OK", () => { + const resolution = "OK"; + const result = transformResolution(resolution); + expect(result).toEqual({ + outcome: "Succeeded", + }); + }) + test("ErrBounce:pie", () => { + const resolution = "ErrBounce:pie"; + const result = transformResolution(resolution); + expect(result).toEqual({ + outcome: "Bounced", + by: "pie", + }); + }) + test("ErrBounce:bul/ec", () => { + const resolution = "ErrBounce:bul/ec"; + const result = transformResolution(resolution); + expect(result).toEqual({ + outcome: "Bounced", + by: "bul/ec", + }); + }) + test("ErrSupportBroken:pie", () => { + const resolution = "ErrSupportBroken:pie"; + const result = transformResolution(resolution); + expect(result).toEqual({ + outcome: "Support broken", + by: "pie", + }); + }) + test("ErrInvalidSupporteeOrder", () => { + const resolution = "ErrInvalidSupporteeOrder"; + const result = transformResolution(resolution); + expect(result).toEqual({ + outcome: "Invalid order", + }); + }) +}); diff --git a/src/common/schema/list-phases.ts b/src/common/schema/list-phases.ts index 55235cc0..480446ea 100644 --- a/src/common/schema/list-phases.ts +++ b/src/common/schema/list-phases.ts @@ -1,5 +1,6 @@ import { z } from "zod"; import { apiResponseSchema, listApiResponseSchema } from "./common"; +import { transformResolution } from "../../util"; const unitSchema = z.object({ Type: z.string(), @@ -36,6 +37,7 @@ const resolutionSchema = z.object({ Resolution: z.string(), }); + const phaseSchema = z.object({ PhaseOrdinal: z.number(), Season: z.string(), @@ -57,7 +59,13 @@ const phaseSchema = z.object({ Dislodgers: z.array(dislodgerSchema).nullable(), ForceDisbands: z.array(z.string()).nullable(), Bounces: z.array(bounceSchema).nullable(), - Resolutions: z.array(resolutionSchema).nullable(), + Resolutions: z.union([z.array(resolutionSchema), z.null()]).transform((data) => { + if (data === null) return []; + return data.map((resolution) => { + const { outcome, by } = transformResolution(resolution.Resolution); + return { province: resolution.Province, outcome, by }; + }) + }), Host: z.string(), SoloSCCount: z.number(), }); diff --git a/src/components/orders/orders-utils.ts b/src/components/orders/orders-utils.ts index f869c149..67d6f24f 100644 --- a/src/components/orders/orders-utils.ts +++ b/src/components/orders/orders-utils.ts @@ -1,13 +1,14 @@ import { useGetOrdersQuery, useGetPhaseQuery, useGetVariantQuery } from "../../common"; -type Outcome = "OK" | "Bounced" | "SupportBroken" | "Invalid" | undefined; - type Order = { source: string; orderType: string; target: string | undefined; aux: string | undefined; - outcome: Outcome; + outcome: { + outcome: string; + by?: string; + } | undefined; }; const createOrders = ( @@ -21,16 +22,17 @@ const createOrders = ( const [source, orderType, target, aux] = order.Parts; if (!source) throw new Error("No source found"); if (!orderType) throw new Error("No orderType found"); - const resolution = phase.Resolutions?.find((resolution) => resolution.Province === source); - - const outcome: Outcome = resolution?.Resolution.includes("OK") ? "OK" : resolution?.Resolution.includes("Bounce") ? "Bounced" : resolution?.Resolution.includes("SupportBroken") ? "SupportBroken" : resolution?.Resolution.includes("Invalid") ? "Invalid" : undefined; + const outcome = phase.Resolutions?.find((resolution) => resolution.province === source); const orderData = { source: variant.getProvinceLongName(source), orderType: orderType, target: target ? variant.getProvinceLongName(target) : undefined, aux: aux ? variant.getProvinceLongName(aux) : undefined, - outcome: outcome + outcome: outcome ? { + outcome: outcome.outcome, + by: outcome.by ? variant.getProvinceLongName(outcome.by) : undefined, + } : undefined, }; if (!ordersByNation.has(order.Nation)) { diff --git a/src/components/orders/orders.tsx b/src/components/orders/orders.tsx index 4f017221..d43dadca 100644 --- a/src/components/orders/orders.tsx +++ b/src/components/orders/orders.tsx @@ -11,20 +11,12 @@ import { Typography, useTheme, } from "@mui/material"; - import { CheckCircle as OrderOkIcon, Cancel as OrderErrorIcon, } from "@mui/icons-material"; - import { useOrders } from "./use-orders"; - -type OrderProps = { - source: string; - orderType: string; - target?: string; - aux?: string; -}; +import { formatOrderText } from "../../util"; const StyledListItem = styled(ListItem)(({ theme }) => ({ border: `1px solid ${theme.palette.grey[200]}`, @@ -37,86 +29,65 @@ const StyledListSubheader = styled(ListSubheader)(({ theme }) => ({ backgroundColor: theme.palette.grey[200], })); -const formatOrderText = (order: OrderProps) => { - if (order.orderType === "Hold") { - return `${order.source} Hold`; - } - if (order.orderType === "Support") { - return `${order.source} Support ${order.target} ${order.aux}`; - } - return `${order.source} ${order.orderType} to ${order.target}`; -}; - -const OutcomeLabelMap = { - OK: "Succeeded", - Bounced: "Bounced", - SupportBroken: "Support broken", - Invalid: "Invalid order", -} as const; +const StyledContainer = styled(Stack)(({ theme }) => ({ + maxWidth: 630, + width: "100%", + margin: "0 auto", + gap: theme.spacing(2), + padding: theme.spacing(2), +})); const Orders: React.FC = () => { const theme = useTheme(); - const { isLoading, isError, orders } = useOrders(); + const { isLoading, orders } = useOrders(); if (isLoading) { return ( - + - + ); } - if (isError) { - return ( - - Error loading orders - - ); - } + if (!orders) return null; return ( - + Orders {orders.map(({ nation, orders }) => ( - - {nation}}> - {orders.map((order, index) => ( - + {nation}} + > + {orders.map((order, index) => ( + + {order.outcome && ( + + {order.outcome.outcome === "Succeeded" ? ( + + ) : ( + + )} + + )} + + + {formatOrderText(order)} + {order.outcome && ( - - {order.outcome === "OK" ? ( - - ) : ( - - )} - - )} - - - {formatOrderText(order)} + + {order.outcome.outcome} + {order.outcome.by ? ` by ${order.outcome.by}` : ""} - {order.outcome && ( - - - {OutcomeLabelMap[order.outcome]} - - - )} - - - ))} - - + )} + + + ))} + ))} - + ); }; diff --git a/src/util.ts b/src/util.ts new file mode 100644 index 00000000..dbb37c25 --- /dev/null +++ b/src/util.ts @@ -0,0 +1,41 @@ +/** + * Formats an order object into a human-readable string. + */ +const formatOrderText = (order: { + source: string; + orderType: string; + target?: string; + aux?: string; +}) => { + if (order.orderType === "Hold") { + return `${order.source} Hold`; + } + if (order.orderType === "Support") { + return `${order.source} Support ${order.target} ${order.aux}`; + } + return `${order.source} ${order.orderType} to ${order.target}`; +} + +/** + * Transforms a resolution string into a human-readable label + * and extracts `by` if it exists. + */ +const transformResolution = (resolution: string): { outcome: string, by?: string } => { + const resolutionMap: Record = { + OK: "Succeeded", + ErrBounce: "Bounced", + ErrSupportBroken: "Support broken", + ErrInvalidSupporteeOrder: "Invalid order", + } + + const regex = /([^:]+)(?::(.+))?/; + + const match = resolution.match(regex); + if (!match) throw new Error(`Unexpected resolution: ${resolution}`); + return { + outcome: resolutionMap[match[1]], + by: match[2], + }; +}; + +export { formatOrderText, transformResolution }; \ No newline at end of file From edc65020511e989b488399183afc63385fddf029 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Mon, 27 Jan 2025 15:44:51 +0000 Subject: [PATCH 09/64] wip --- src/Router.tsx | 18 +- src/common/store/service.ts | 5 +- .../drawer-navigation/drawer-navigation.tsx | 102 +++++++++ src/components/drawer-navigation/index.ts | 1 + src/components/game-card/game-card.tsx | 7 +- src/components/home/home.tsx | 72 ------- src/components/home/index.ts | 1 - src/components/home/use-home.ts | 35 ---- src/components/index.ts | 2 + src/components/query-container.tsx | 54 +++++ src/screens/CreateGame.tsx | 128 ++++++------ src/screens/home/create-game/create-game.tsx | 151 ++++++++++++++ src/screens/home/create-game/index.ts | 1 + .../home/create-game/use-create-game.ts | 0 src/screens/home/find-games/find-games.tsx | 88 ++++++++ src/screens/home/find-games/index.ts | 1 + src/screens/home/find-games/use-find-games.ts | 18 ++ src/screens/home/index.ts | 5 + src/screens/home/layout.tsx | 195 ++++++++++++++++++ src/screens/home/my-games/index.ts | 1 + src/screens/home/my-games/my-games.tsx | 163 +++++++++++++++ src/screens/home/my-games/use-my-games.ts | 36 ++++ src/screens/home/profile/index.ts | 1 + src/screens/home/profile/profile.tsx | 89 ++++++++ src/screens/home/profile/use-profile.ts | 8 + src/screens/home/screen-title.tsx | 34 +++ src/screens/index.ts | 1 + src/theme.ts | 6 +- 28 files changed, 1041 insertions(+), 182 deletions(-) create mode 100644 src/components/drawer-navigation/drawer-navigation.tsx create mode 100644 src/components/drawer-navigation/index.ts delete mode 100644 src/components/home/home.tsx delete mode 100644 src/components/home/index.ts delete mode 100644 src/components/home/use-home.ts create mode 100644 src/components/index.ts create mode 100644 src/components/query-container.tsx create mode 100644 src/screens/home/create-game/create-game.tsx create mode 100644 src/screens/home/create-game/index.ts create mode 100644 src/screens/home/create-game/use-create-game.ts create mode 100644 src/screens/home/find-games/find-games.tsx create mode 100644 src/screens/home/find-games/index.ts create mode 100644 src/screens/home/find-games/use-find-games.ts create mode 100644 src/screens/home/index.ts create mode 100644 src/screens/home/layout.tsx create mode 100644 src/screens/home/my-games/index.ts create mode 100644 src/screens/home/my-games/my-games.tsx create mode 100644 src/screens/home/my-games/use-my-games.ts create mode 100644 src/screens/home/profile/index.ts create mode 100644 src/screens/home/profile/profile.tsx create mode 100644 src/screens/home/profile/use-profile.ts create mode 100644 src/screens/home/screen-title.tsx create mode 100644 src/screens/index.ts diff --git a/src/Router.tsx b/src/Router.tsx index 01226da6..632e5309 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,10 +1,8 @@ import React from "react"; -import CreateGame from "./screens/CreateGame"; import { Navigate, Outlet, Route, Routes, useNavigate } from "react-router"; import Login from "./screens/Login"; import { useSelector } from "react-redux"; import { selectAuth } from "./common/store/auth"; -import UserPage from "./screens/UserPage"; import { Map } from "./components/Map"; import { GameDetailsLayout } from "./components/GameDetailsLayout"; import { GameDetailsNavigation } from "./components/GameDetailsNavigation"; @@ -13,11 +11,15 @@ import { CreateOrder } from "./components/CreateOrder"; import { Modal } from "./components/Modal"; import { CreateOrderAction } from "./components/CreateOrderAction"; import { ConfirmOrdersAction } from "./components/ConfirmOrdersAction"; -import { Home } from "./components/home"; -import { HomeLayout } from "./components/home-layout"; import { PlayerInfo } from "./components/PlayerInfo"; import { GameInfo } from "./components/GameInfo"; -import { BrowseGames } from "./components/browse-games"; +import { + CreateGame, + FindGames, + Layout as HomeLayout, + MyGames, + Profile, +} from "./screens"; const Router: React.FC = () => { const { loggedIn } = useSelector(selectAuth); @@ -33,10 +35,10 @@ const Router: React.FC = () => { }> }> - } /> - } /> + } /> + } /> } /> - } /> + } /> diff --git a/src/common/store/service.ts b/src/common/store/service.ts index 46cacced..e0e80bf5 100644 --- a/src/common/store/service.ts +++ b/src/common/store/service.ts @@ -154,7 +154,7 @@ const newGameSchema = z.object({ const baseUrl = "https://diplicity-engine.appspot.com/"; -export default createApi({ +const service = createApi({ tagTypes: [ TagType.Game, TagType.ListGames, @@ -471,7 +471,8 @@ export default createApi({ type Variant = z.infer; -export { newGameSchema, gameSchema, apiResponseSchema, extractProperties, extractPropertiesList, listApiResponseSchema } +export { newGameSchema, gameSchema, apiResponseSchema, extractProperties, extractPropertiesList, listApiResponseSchema, service } export type { Variant } +export default service; diff --git a/src/components/drawer-navigation/drawer-navigation.tsx b/src/components/drawer-navigation/drawer-navigation.tsx new file mode 100644 index 00000000..792c9028 --- /dev/null +++ b/src/components/drawer-navigation/drawer-navigation.tsx @@ -0,0 +1,102 @@ +import React from "react"; +import { + Drawer, + List, + ListItem, + ListItemButton, + ListItemIcon, + ListItemText, + useTheme, +} from "@mui/material"; + +const drawerWidth = 240; + +const DrawerNavigationContext = React.createContext< + | { + value: string; + onChange: (newValue: string) => void; + } + | undefined +>(undefined); + +type DrawerNavigationProps = React.PropsWithChildren<{ + value: string; + onChange: (newValue: string) => void; +}>; + +const DrawerNavigation: React.FC = (props) => { + return ( + + + {props.children} + + + ); +}; + +type DrawerNavigationActionProps = React.PropsWithChildren<{ + label: string; + icon: React.ReactElement; + value: string; +}>; + +const DrawerNavigationAction: React.FC = ( + props +) => { + const theme = useTheme(); + const context = React.useContext(DrawerNavigationContext); + + if (!context) { + throw new Error( + "DrawerNavigationAction must be used within a DrawerNavigation" + ); + } + + const selectedItemIconStyle = { + color: theme.palette.primary.main, + }; + + const selectedItemTextStyle = { + color: theme.palette.primary.main, + fontWeight: "bold", + }; + + const selected = context?.value === props.value; + + return ( + + context.onChange(props.value)}> + + {React.cloneElement(props.icon, { + style: selected ? selectedItemIconStyle : {}, + })} + + + + + ); +}; + +export { DrawerNavigation, DrawerNavigationAction }; diff --git a/src/components/drawer-navigation/index.ts b/src/components/drawer-navigation/index.ts new file mode 100644 index 00000000..2152fb30 --- /dev/null +++ b/src/components/drawer-navigation/index.ts @@ -0,0 +1 @@ +export * from "./drawer-navigation"; \ No newline at end of file diff --git a/src/components/game-card/game-card.tsx b/src/components/game-card/game-card.tsx index 6b2954aa..86f244a4 100644 --- a/src/components/game-card/game-card.tsx +++ b/src/components/game-card/game-card.tsx @@ -24,12 +24,15 @@ import { Chip, Avatar, Link, + styled, } from "@mui/material"; import { useGameCard } from "./use-game-card"; const MAX_AVATARS = 7; const AVATAR_SIZE = 28; +const StyledCard = styled(Card)(() => ({})); + const GameCard: React.FC<{ canJoin: boolean; canLeave: boolean; @@ -82,7 +85,7 @@ const GameCard: React.FC<{ const remainingUsersCount = props.members.length - displayedUsers.length; return ( - + @@ -250,7 +253,7 @@ const GameCard: React.FC<{ - + ); }; diff --git a/src/components/home/home.tsx b/src/components/home/home.tsx deleted file mode 100644 index 39c976ea..00000000 --- a/src/components/home/home.tsx +++ /dev/null @@ -1,72 +0,0 @@ -import React from "react"; -import { CircularProgress, Stack, Typography } from "@mui/material"; -import { useHome } from "./use-home"; -import { GameCard } from "../game-card"; - -const Layout: React.FC<{ - children: React.ReactNode; -}> = (props) => { - return ( - - My games - {props.children} - - ); -}; - -const Home: React.FC = () => { - const { isLoading, isError, data } = useHome(); - - if (isLoading) { - return ( - - - - ); - } - - if (isError) { - return Error; - } - - if (!data) throw new Error("No data"); - - return ( - - - {data.stagingGameCards.length > 0 && ( - - Staging Games - - {data.stagingGameCards.map((game) => ( - - ))} - - - )} - {data.startedGameCards.length > 0 && ( - - Started Games - - {data.startedGameCards.map((game) => ( - - ))} - - - )} - {data.finishedGameCards.length > 0 && ( - - Finished Games - - {data.finishedGameCards.map((game) => ( - - ))} - - - )} - - - ); -}; - -export { Home }; diff --git a/src/components/home/index.ts b/src/components/home/index.ts deleted file mode 100644 index 61a65b75..00000000 --- a/src/components/home/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./home"; \ No newline at end of file diff --git a/src/components/home/use-home.ts b/src/components/home/use-home.ts deleted file mode 100644 index 3ea4b4a5..00000000 --- a/src/components/home/use-home.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { mergeQueries } from "../../common"; -import service from "../../common/store/service"; -import { createGameCardProps } from "../game-card"; - -const options = { my: true, mastered: false }; - -const useHome = () => { - const { endpoints } = service - const listStagingGamesQuery = endpoints.listGames.useQuery( - { - ...options, - status: "Staging", - }, - ); - const listStartedGamesQuery = endpoints.listGames.useQuery({ - ...options, - status: "Started", - }); - const listFinishedGamesQuery = endpoints.listGames.useQuery({ - ...options, - status: "Finished", - }); - return mergeQueries([listStagingGamesQuery, listStartedGamesQuery, listFinishedGamesQuery], (stagingGames, startedGames, finishedGames) => { - const stagingGameCards = stagingGames.map(createGameCardProps) - const startedGameCards = startedGames.map(createGameCardProps) - const finishedGameCards = finishedGames.map(createGameCardProps) - return { - stagingGameCards, - startedGameCards, - finishedGameCards, - } - }); -} - -export { useHome }; \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 00000000..4ece7d34 --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,2 @@ +export * from "./drawer-navigation" +export * from "./query-container" \ No newline at end of file diff --git a/src/components/query-container.tsx b/src/components/query-container.tsx new file mode 100644 index 00000000..6f2f2c65 --- /dev/null +++ b/src/components/query-container.tsx @@ -0,0 +1,54 @@ +import React from "react"; +import { CircularProgress, Box, Typography } from "@mui/material"; + +type Query = { + isLoading: boolean; + isError: boolean; + data?: TData; +}; + +type QueryContainerProps = { + query: Query; + children: (data: TData) => React.ReactNode; +}; + +const QueryContainer = ({ + query, + children, +}: QueryContainerProps) => { + if (query.isLoading) { + return ( + + + + ); + } + + if (query.isError) { + return ( + + + An error occurred while loading data. + + + ); + } + + if (query.data === undefined) { + return null; + } + + return <>{children(query.data)}; +}; + +export { QueryContainer }; diff --git a/src/screens/CreateGame.tsx b/src/screens/CreateGame.tsx index 1a6a02f8..55d27c3e 100644 --- a/src/screens/CreateGame.tsx +++ b/src/screens/CreateGame.tsx @@ -8,6 +8,8 @@ import { FormControlLabel, Checkbox, Button, + styled, + Box, } from "@mui/material"; import { toFormikValidationSchema } from "zod-formik-adapter"; import service, { newGameSchema } from "../common/store/service"; @@ -16,6 +18,12 @@ import { useDispatch } from "react-redux"; import { feedbackActions } from "../common/store/feedback"; import { useNavigate } from "react-router"; +const StyledBox = styled(Box)(({ theme }) => ({ + height: "100%", + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, +})); + const CreateGame: React.FC = () => { const listVariantsQuery = service.endpoints.listVariants.useQuery(undefined); @@ -81,65 +89,67 @@ const CreateGame: React.FC = () => { } return ( -
{ - e.preventDefault(); - formik.handleSubmit(e); - }} - > - - Create Game - formik.setFieldValue("Desc", e.target.value)} - onBlur={formik.handleBlur} - error={formik.touched.Desc && Boolean(formik.errors.Desc)} - helperText={formik.touched.Desc && formik.errors.Desc} - disabled={createGameQuery.isLoading} - /> - formik.setFieldValue("Variant", e.target.value)} - onBlur={formik.handleBlur} - error={formik.touched.Variant && Boolean(formik.errors.Variant)} - helperText={formik.touched.Variant && formik.errors.Variant} - disabled={createGameQuery.isLoading} - > - {listVariantsQuery.data?.map((variant) => ( - - {variant.Name} - - ))} - - - } - label="Private" - /> - - -
+ +
{ + e.preventDefault(); + formik.handleSubmit(e); + }} + > + + Create Game + formik.setFieldValue("Desc", e.target.value)} + onBlur={formik.handleBlur} + error={formik.touched.Desc && Boolean(formik.errors.Desc)} + helperText={formik.touched.Desc && formik.errors.Desc} + disabled={createGameQuery.isLoading} + /> + formik.setFieldValue("Variant", e.target.value)} + onBlur={formik.handleBlur} + error={formik.touched.Variant && Boolean(formik.errors.Variant)} + helperText={formik.touched.Variant && formik.errors.Variant} + disabled={createGameQuery.isLoading} + > + {listVariantsQuery.data?.map((variant) => ( + + {variant.Name} + + ))} + + + } + label="Private" + /> + + +
+
); }; diff --git a/src/screens/home/create-game/create-game.tsx b/src/screens/home/create-game/create-game.tsx new file mode 100644 index 00000000..8355260c --- /dev/null +++ b/src/screens/home/create-game/create-game.tsx @@ -0,0 +1,151 @@ +import React from "react"; +import { useFormik } from "formik"; +import { + Stack, + TextField, + MenuItem, + FormControlLabel, + Checkbox, + Button, +} from "@mui/material"; +import { toFormikValidationSchema } from "zod-formik-adapter"; +import service, { newGameSchema } from "../../../common/store/service"; +import { feedbackActions } from "../../../common/store/feedback"; +import { z } from "zod"; +import { useDispatch } from "react-redux"; +import { useNavigate } from "react-router"; +import { QueryContainer } from "../../../components"; +import { ScreenTopBar } from "../screen-title"; + +const CreateGame: React.FC = () => { + const listVariantsQuery = service.endpoints.listVariants.useQuery(undefined); + + const [createGameMutationTrigger, createGameQuery] = + service.endpoints.createGame.useMutation(); + + const dispatch = useDispatch(); + const navigate = useNavigate(); + + const formik = useFormik>({ + initialValues: { + Anonymous: false, + ChatLanguageISO639_1: "", + Desc: "", + DisableConferenceChat: false, + DisableGroupChat: false, + DisablePrivateChat: false, + FirstMember: { + NationPreferences: "", + }, + GameMasterEnabled: false, + LastYear: 0, + MaxHated: null, + MaxHater: 0, + MaxRating: 0, + MinQuickness: 0, + MinRating: 0, + MinReliability: 0, + NationAllocation: 0, + NonMovementPhaseLengthMinutes: 0, + PhaseLengthMinutes: 1440, + Private: false, + RequireGameMasterInvitation: false, + SkipMuster: true, + Variant: "Classical", + }, + validationSchema: toFormikValidationSchema(newGameSchema), + onSubmit: async (values) => { + try { + await createGameMutationTrigger({ + ...values, + }).unwrap(); + navigate("/"); + dispatch( + feedbackActions.setFeedback({ + severity: "success", + message: "Game created successfully", + }) + ); + } catch { + dispatch( + feedbackActions.setFeedback({ + severity: "error", + message: "Something went wrong", + }) + ); + } + }, + }); + + return ( + <> + +
{ + e.preventDefault(); + formik.handleSubmit(e); + }} + > + + {(data) => ( + + formik.setFieldValue("Desc", e.target.value)} + onBlur={formik.handleBlur} + error={formik.touched.Desc && Boolean(formik.errors.Desc)} + helperText={formik.touched.Desc && formik.errors.Desc} + disabled={createGameQuery.isLoading} + /> + + formik.setFieldValue("Variant", e.target.value) + } + onBlur={formik.handleBlur} + error={formik.touched.Variant && Boolean(formik.errors.Variant)} + helperText={formik.touched.Variant && formik.errors.Variant} + disabled={createGameQuery.isLoading} + > + {data.map((variant) => ( + + {variant.Name} + + ))} + + + } + label="Private" + /> + + + )} + +
+ + ); +}; + +export { CreateGame }; diff --git a/src/screens/home/create-game/index.ts b/src/screens/home/create-game/index.ts new file mode 100644 index 00000000..e7ae89b9 --- /dev/null +++ b/src/screens/home/create-game/index.ts @@ -0,0 +1 @@ +export * from "./create-game" \ No newline at end of file diff --git a/src/screens/home/create-game/use-create-game.ts b/src/screens/home/create-game/use-create-game.ts new file mode 100644 index 00000000..e69de29b diff --git a/src/screens/home/find-games/find-games.tsx b/src/screens/home/find-games/find-games.tsx new file mode 100644 index 00000000..d5e8d629 --- /dev/null +++ b/src/screens/home/find-games/find-games.tsx @@ -0,0 +1,88 @@ +import React from "react"; +import { + Stack, + Typography, + IconButton, + Avatar, + Button, + Link, + ListItem, + ListItemAvatar, + ListItemText, + List, +} from "@mui/material"; +import { MoreHoriz as MenuIcon } from "@mui/icons-material"; +import { QueryContainer } from "../../../components"; +import { useFindGames } from "./use-find-games"; +import { ScreenTopBar } from "../screen-title"; + +const FindGames: React.FC = () => { + const query = useFindGames(); + + return ( + <> + + + + {(games) => + games.map((game) => ( + + + + } + > + + + + + {game.Desc} + + } + secondary={ + + + + {game.Variant} + + + {game.PhaseLengthMinutes} + + + + + } + /> + + )) + } + + + + ); +}; + +export { FindGames }; diff --git a/src/screens/home/find-games/index.ts b/src/screens/home/find-games/index.ts new file mode 100644 index 00000000..49052da3 --- /dev/null +++ b/src/screens/home/find-games/index.ts @@ -0,0 +1 @@ +export * from "./find-games" \ No newline at end of file diff --git a/src/screens/home/find-games/use-find-games.ts b/src/screens/home/find-games/use-find-games.ts new file mode 100644 index 00000000..63124944 --- /dev/null +++ b/src/screens/home/find-games/use-find-games.ts @@ -0,0 +1,18 @@ +import { mergeQueries, service } from "../../../common"; + +const options = { my: false, mastered: false }; + +const useFindGames = () => { + const { endpoints } = service + const listOpenGamesQuery = endpoints.listGames.useQuery( + { + ...options, + status: "Open", + }, + ); + return mergeQueries([listOpenGamesQuery], (games) => { + return games + }); +} + +export { useFindGames }; \ No newline at end of file diff --git a/src/screens/home/index.ts b/src/screens/home/index.ts new file mode 100644 index 00000000..e50caf31 --- /dev/null +++ b/src/screens/home/index.ts @@ -0,0 +1,5 @@ +export * from "./layout"; +export * from "./find-games" +export * from "./create-game" +export * from "./profile" +export * from "./my-games"; \ No newline at end of file diff --git a/src/screens/home/layout.tsx b/src/screens/home/layout.tsx new file mode 100644 index 00000000..c09a425d --- /dev/null +++ b/src/screens/home/layout.tsx @@ -0,0 +1,195 @@ +import React, { useState, useEffect } from "react"; +import { + AppBar, + BottomNavigation, + BottomNavigationAction, + useTheme, + useMediaQuery, + Grid2, + Stack, + Typography, + styled, + Box, +} from "@mui/material"; +import { Outlet, useLocation, useNavigate } from "react-router"; +import { + Home as MyGamesIcon, + Search as FindGamesIcon, + Add as CreateGameIcon, + Person as ProfileIcon, +} from "@mui/icons-material"; +import { DrawerNavigation, DrawerNavigationAction } from "../../components"; + +const NavigationItems = [ + { label: "My Games", icon: , value: "/" }, + { label: "Find Games", icon: , value: "/find-games" }, + { label: "Create Game", icon: , value: "/create-game" }, + { label: "Profile", icon: , value: "/profile" }, +] as const; + +const ScreenContainer = styled(Box)(({ theme }) => ({ + height: "100%", + border: `1px solid ${theme.palette.divider}`, + borderRadius: theme.shape.borderRadius, +})); + +const Layout: React.FC = () => { + const navigate = useNavigate(); + const location = useLocation(); + const theme = useTheme(); + const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const [navigation, setNavigation] = useState(location.pathname); + + useEffect(() => { + setNavigation(location.pathname); + }, [location.pathname]); + + const handleNavigationChange = (newValue: string) => { + setNavigation(newValue); + navigate(newValue); + }; + + return ( + <> + {isMobile ? ( + <> + + + handleNavigationChange(newValue)} + > + {NavigationItems.map((item) => ( + + ))} + + + + ) : ( + + + {/* Drawer navigation */} + + + {NavigationItems.map((item) => ( + + ))} + + + {/* Main content */} + + + + + + + + {/* About panel */} + + + + Welcome to Diplicity! + + + If you're new to the game, you can learn more about it{" "} + + here + + . + + + To chat with the developers or meet other players, join our{" "} + + Discord server + + . + + + We massively appreciate your support and feedback! If you have + any questions or suggestions, please let us know by sending an{" "} + + email + + . + + + + + + )} + + ); +}; + +export { Layout }; diff --git a/src/screens/home/my-games/index.ts b/src/screens/home/my-games/index.ts new file mode 100644 index 00000000..bb8289e0 --- /dev/null +++ b/src/screens/home/my-games/index.ts @@ -0,0 +1 @@ +export * from "./my-games" \ No newline at end of file diff --git a/src/screens/home/my-games/my-games.tsx b/src/screens/home/my-games/my-games.tsx new file mode 100644 index 00000000..e3af7582 --- /dev/null +++ b/src/screens/home/my-games/my-games.tsx @@ -0,0 +1,163 @@ +import React from "react"; +import { + Avatar, + Box, + Button, + IconButton, + Link, + List, + ListItem, + ListItemAvatar, + ListItemText, + Stack, + styled, + Tab, + Tabs, + Typography, +} from "@mui/material"; +import { + MoreHoriz as MenuIcon, + AddCircleOutline as StagingIcon, + PlayCircleOutline as StartedIcon, + StopCircleOutlined as FinishedIcon, + SportsMotorsports as DiplicityIcon, +} from "@mui/icons-material"; +import { useMyGames } from "./use-my-games"; +import { QueryContainer } from "../../../components"; + +const StyledTabs = styled((props: React.ComponentProps) => ( + }} + /> +))(({ theme }) => ({ + "& .MuiTabs-indicator": { + display: "flex", + justifyContent: "center", + backgroundColor: "transparent", + }, + "& .MuiTabs-indicatorSpan": { + maxWidth: 40, + width: "100%", + backgroundColor: theme.palette.primary.main, + }, +})); + +const statuses = [ + { value: "staging", label: "Staging", icon: }, + { value: "started", label: "Started", icon: }, + { value: "finished", label: "Finished", icon: }, +] as const; + +type Status = (typeof statuses)[number]["value"]; + +const MyGames: React.FC = () => { + const [selectedStatus, setSelectedStatus] = React.useState< + Status | undefined + >(undefined); + + const query = useMyGames(); + + const status = query.data + ? selectedStatus + ? selectedStatus + : query.data?.startedGames.length > 0 + ? "started" + : query.data?.stagingGames.length > 0 + ? "staging" + : "finished" + : undefined; + + return ( + + + + + + setSelectedStatus(value)} + variant="fullWidth" + sx={{ width: "100%" }} + > + {statuses.map((status) => ( + + ))} + + + + + + {(data) => { + const games = + status === "staging" + ? data.stagingGames + : status === "started" + ? data.startedGames + : data.finishedGames; + + return games.map((game) => ( + + + + } + > + + {game.Variant} + + + {game.Desc} + + } + secondary={ + + + {game.Variant} + + {game.PhaseLengthMinutes} + + + + + } + /> + + )); + }} + + + ); +}; + +export { MyGames }; diff --git a/src/screens/home/my-games/use-my-games.ts b/src/screens/home/my-games/use-my-games.ts new file mode 100644 index 00000000..ad833a9b --- /dev/null +++ b/src/screens/home/my-games/use-my-games.ts @@ -0,0 +1,36 @@ +import { mergeQueries, service } from "../../../common"; + +const options = { my: true, mastered: false }; + +const useMyGames = () => { + const { endpoints } = service + const listVariantsQuery = endpoints.listVariants.useQuery(undefined); + const listStagingGamesQuery = endpoints.listGames.useQuery( + { + ...options, + status: "Staging", + }, + ); + const listStartedGamesQuery = endpoints.listGames.useQuery({ + ...options, + status: "Started", + }); + const listFinishedGamesQuery = endpoints.listGames.useQuery({ + ...options, + status: "Finished", + }); + return mergeQueries([listVariantsQuery, listStagingGamesQuery, listStartedGamesQuery, listFinishedGamesQuery], (variants, stagingGames, startedGames, finishedGames) => { + const getMapSvgUrl = (game: typeof stagingGames[number]) => { + const variant = variants.find((variant) => variant.Name === game.Variant); + return variant?.Links?.find((link) => link.Rel === "map")?.URL + } + return { + getMapSvgUrl, + stagingGames, + startedGames, + finishedGames, + } + }); +} + +export { useMyGames }; \ No newline at end of file diff --git a/src/screens/home/profile/index.ts b/src/screens/home/profile/index.ts new file mode 100644 index 00000000..5300370a --- /dev/null +++ b/src/screens/home/profile/index.ts @@ -0,0 +1 @@ +export * from "./profile"; \ No newline at end of file diff --git a/src/screens/home/profile/profile.tsx b/src/screens/home/profile/profile.tsx new file mode 100644 index 00000000..8057936a --- /dev/null +++ b/src/screens/home/profile/profile.tsx @@ -0,0 +1,89 @@ +import React, { useState } from "react"; +import { + Avatar, + Card, + CardContent, + Grid2, + IconButton, + Menu, + MenuItem, + Stack, + Typography, +} from "@mui/material"; +import { MoreHoriz } from "@mui/icons-material"; +import { useProfile } from "./use-profile"; +import { QueryContainer } from "../../../components"; +import { AppDispatch } from "../../../common"; +import { useDispatch } from "react-redux"; +import { authActions } from "../../../common/store/auth"; +import { ScreenTopBar } from "../screen-title"; + +const Profile: React.FC = () => { + const query = useProfile(); + + const dispatch = useDispatch(); + + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleMenuClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleMenuClose = () => { + setAnchorEl(null); + }; + + const withMenuClose = (fn: () => void) => { + return () => { + fn(); + handleMenuClose(); + }; + }; + + const onClickLogout = () => { + dispatch(authActions.logout()); + }; + + return ( + <> + + + {(data) => ( + + + + + + + + + + {data.Name} + + + + + + + + + Logout + + + + + + + + )} + + + ); +}; + +export { Profile }; diff --git a/src/screens/home/profile/use-profile.ts b/src/screens/home/profile/use-profile.ts new file mode 100644 index 00000000..c9b90c69 --- /dev/null +++ b/src/screens/home/profile/use-profile.ts @@ -0,0 +1,8 @@ +import { service } from "../../../common"; + +const useProfile = () => { + const rootQuery = service.endpoints.getRoot.useQuery(undefined); + return rootQuery; +} + +export { useProfile }; \ No newline at end of file diff --git a/src/screens/home/screen-title.tsx b/src/screens/home/screen-title.tsx new file mode 100644 index 00000000..8470b88d --- /dev/null +++ b/src/screens/home/screen-title.tsx @@ -0,0 +1,34 @@ +import { IconButton, Stack, Typography } from "@mui/material"; +import { KeyboardBackspace as BackIcon } from "@mui/icons-material"; +import { useNavigate } from "react-router"; + +type ScreenTopBarProps = { + title: string; +}; + +const ScreenTopBar: React.FC = ({ title }) => { + const navigate = useNavigate(); + + return ( + ({ borderBottom: `1px solid ${theme.palette.divider}` })} + > + navigate("/")} + sx={(theme) => ({ + padding: 0, + color: theme.palette.text.primary, + })} + > + + + {title} + + ); +}; + +export { ScreenTopBar }; diff --git a/src/screens/index.ts b/src/screens/index.ts new file mode 100644 index 00000000..32c3e8b8 --- /dev/null +++ b/src/screens/index.ts @@ -0,0 +1 @@ +export * from "./home" \ No newline at end of file diff --git a/src/theme.ts b/src/theme.ts index 286a86ba..f853c76d 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -3,9 +3,9 @@ import { createTheme } from "@mui/material"; const theme = createTheme({ typography: { h1: { - fontSize: "28px", - lineHeight: "34px", - fontWeight: 600, + fontSize: "18px", + lineHeight: "22px", + fontWeight: 800, marginBottom: "8px", }, h2: { From 677097032c1b0ccb50e58a2b2137189ea8a38355 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Mon, 27 Jan 2025 15:57:29 +0000 Subject: [PATCH 10/64] Refactor game-related hooks and components; remove unused files and consolidate logic in useFindGames and useMyGames --- src/common/store/index.ts | 2 + .../home/{create-game => }/create-game.tsx | 113 ++++++++---------- src/screens/home/create-game/index.ts | 1 - .../home/create-game/use-create-game.ts | 0 .../home/{find-games => }/find-games.tsx | 19 ++- src/screens/home/find-games/index.ts | 1 - src/screens/home/find-games/use-find-games.ts | 18 --- src/screens/home/{my-games => }/my-games.tsx | 50 +++++++- src/screens/home/my-games/index.ts | 1 - src/screens/home/my-games/use-my-games.ts | 36 ------ src/screens/home/{profile => }/profile.tsx | 27 +++-- src/screens/home/profile/index.ts | 1 - src/screens/home/profile/use-profile.ts | 8 -- .../{screen-title.tsx => screen-top-bar.tsx} | 0 14 files changed, 129 insertions(+), 148 deletions(-) rename src/screens/home/{create-game => }/create-game.tsx (59%) delete mode 100644 src/screens/home/create-game/index.ts delete mode 100644 src/screens/home/create-game/use-create-game.ts rename src/screens/home/{find-games => }/find-games.tsx (86%) delete mode 100644 src/screens/home/find-games/index.ts delete mode 100644 src/screens/home/find-games/use-find-games.ts rename src/screens/home/{my-games => }/my-games.tsx (78%) delete mode 100644 src/screens/home/my-games/index.ts delete mode 100644 src/screens/home/my-games/use-my-games.ts rename src/screens/home/{profile => }/profile.tsx (80%) delete mode 100644 src/screens/home/profile/index.ts delete mode 100644 src/screens/home/profile/use-profile.ts rename src/screens/home/{screen-title.tsx => screen-top-bar.tsx} (100%) diff --git a/src/common/store/index.ts b/src/common/store/index.ts index eeedb81e..25f6b09d 100644 --- a/src/common/store/index.ts +++ b/src/common/store/index.ts @@ -1,3 +1,4 @@ +import { authActions } from "./auth"; import { feedbackActions } from "./feedback"; export { selectFeedback } from "./feedback"; export * from "./store"; @@ -6,4 +7,5 @@ export * as Service from "./service.types"; export const actions = { ...feedbackActions, + ...authActions }; \ No newline at end of file diff --git a/src/screens/home/create-game/create-game.tsx b/src/screens/home/create-game.tsx similarity index 59% rename from src/screens/home/create-game/create-game.tsx rename to src/screens/home/create-game.tsx index 8355260c..f5f30fe4 100644 --- a/src/screens/home/create-game/create-game.tsx +++ b/src/screens/home/create-game.tsx @@ -9,72 +9,63 @@ import { Button, } from "@mui/material"; import { toFormikValidationSchema } from "zod-formik-adapter"; -import service, { newGameSchema } from "../../../common/store/service"; -import { feedbackActions } from "../../../common/store/feedback"; +import service, { newGameSchema } from "../../common/store/service"; import { z } from "zod"; -import { useDispatch } from "react-redux"; import { useNavigate } from "react-router"; -import { QueryContainer } from "../../../components"; -import { ScreenTopBar } from "../screen-title"; +import { QueryContainer } from "../../components"; +import { ScreenTopBar } from "./screen-top-bar"; -const CreateGame: React.FC = () => { - const listVariantsQuery = service.endpoints.listVariants.useQuery(undefined); +const initialValues = { + Anonymous: false, + ChatLanguageISO639_1: "", + Desc: "", + DisableConferenceChat: false, + DisableGroupChat: false, + DisablePrivateChat: false, + FirstMember: { + NationPreferences: "", + }, + GameMasterEnabled: false, + LastYear: 0, + MaxHated: null, + MaxHater: 0, + MaxRating: 0, + MinQuickness: 0, + MinRating: 0, + MinReliability: 0, + NationAllocation: 0, + NonMovementPhaseLengthMinutes: 0, + PhaseLengthMinutes: 1440, + Private: false, + RequireGameMasterInvitation: false, + SkipMuster: true, + Variant: "Classical", +}; +const useCreateGame = () => { + const navigate = useNavigate(); + + const query = service.endpoints.listVariants.useQuery(undefined); const [createGameMutationTrigger, createGameQuery] = service.endpoints.createGame.useMutation(); - const dispatch = useDispatch(); - const navigate = useNavigate(); + const handleSubmit = async (values: z.infer) => { + await createGameMutationTrigger({ + ...values, + }).unwrap(); + navigate("/"); + }; + + return { query, handleSubmit, isSubmitting: createGameQuery.isLoading }; +}; + +const CreateGame: React.FC = () => { + const { query, handleSubmit, isSubmitting } = useCreateGame(); const formik = useFormik>({ - initialValues: { - Anonymous: false, - ChatLanguageISO639_1: "", - Desc: "", - DisableConferenceChat: false, - DisableGroupChat: false, - DisablePrivateChat: false, - FirstMember: { - NationPreferences: "", - }, - GameMasterEnabled: false, - LastYear: 0, - MaxHated: null, - MaxHater: 0, - MaxRating: 0, - MinQuickness: 0, - MinRating: 0, - MinReliability: 0, - NationAllocation: 0, - NonMovementPhaseLengthMinutes: 0, - PhaseLengthMinutes: 1440, - Private: false, - RequireGameMasterInvitation: false, - SkipMuster: true, - Variant: "Classical", - }, + initialValues, validationSchema: toFormikValidationSchema(newGameSchema), - onSubmit: async (values) => { - try { - await createGameMutationTrigger({ - ...values, - }).unwrap(); - navigate("/"); - dispatch( - feedbackActions.setFeedback({ - severity: "success", - message: "Game created successfully", - }) - ); - } catch { - dispatch( - feedbackActions.setFeedback({ - severity: "error", - message: "Something went wrong", - }) - ); - } - }, + onSubmit: handleSubmit, }); return ( @@ -86,7 +77,7 @@ const CreateGame: React.FC = () => { formik.handleSubmit(e); }} > - + {(data) => ( { onBlur={formik.handleBlur} error={formik.touched.Desc && Boolean(formik.errors.Desc)} helperText={formik.touched.Desc && formik.errors.Desc} - disabled={createGameQuery.isLoading} + disabled={isSubmitting} /> { onBlur={formik.handleBlur} error={formik.touched.Variant && Boolean(formik.errors.Variant)} helperText={formik.touched.Variant && formik.errors.Variant} - disabled={createGameQuery.isLoading} + disabled={isSubmitting} > {data.map((variant) => ( @@ -127,7 +118,7 @@ const CreateGame: React.FC = () => { checked={formik.values.Private} onChange={formik.handleChange} onBlur={formik.handleBlur} - disabled={createGameQuery.isLoading} + disabled={isSubmitting} /> } label="Private" @@ -136,7 +127,7 @@ const CreateGame: React.FC = () => { variant="contained" color="primary" type="submit" - disabled={createGameQuery.isLoading} + disabled={isSubmitting} > Create diff --git a/src/screens/home/create-game/index.ts b/src/screens/home/create-game/index.ts deleted file mode 100644 index e7ae89b9..00000000 --- a/src/screens/home/create-game/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./create-game" \ No newline at end of file diff --git a/src/screens/home/create-game/use-create-game.ts b/src/screens/home/create-game/use-create-game.ts deleted file mode 100644 index e69de29b..00000000 diff --git a/src/screens/home/find-games/find-games.tsx b/src/screens/home/find-games.tsx similarity index 86% rename from src/screens/home/find-games/find-games.tsx rename to src/screens/home/find-games.tsx index d5e8d629..06957ee9 100644 --- a/src/screens/home/find-games/find-games.tsx +++ b/src/screens/home/find-games.tsx @@ -12,12 +12,23 @@ import { List, } from "@mui/material"; import { MoreHoriz as MenuIcon } from "@mui/icons-material"; -import { QueryContainer } from "../../../components"; -import { useFindGames } from "./use-find-games"; -import { ScreenTopBar } from "../screen-title"; +import { QueryContainer } from "../../components"; +import { ScreenTopBar } from "./screen-top-bar"; +import { service } from "../../common"; + +const options = { my: false, mastered: false }; + +const useFindGames = () => { + const { endpoints } = service; + const query = endpoints.listGames.useQuery({ + ...options, + status: "Open", + }); + return { query }; +}; const FindGames: React.FC = () => { - const query = useFindGames(); + const { query } = useFindGames(); return ( <> diff --git a/src/screens/home/find-games/index.ts b/src/screens/home/find-games/index.ts deleted file mode 100644 index 49052da3..00000000 --- a/src/screens/home/find-games/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./find-games" \ No newline at end of file diff --git a/src/screens/home/find-games/use-find-games.ts b/src/screens/home/find-games/use-find-games.ts deleted file mode 100644 index 63124944..00000000 --- a/src/screens/home/find-games/use-find-games.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { mergeQueries, service } from "../../../common"; - -const options = { my: false, mastered: false }; - -const useFindGames = () => { - const { endpoints } = service - const listOpenGamesQuery = endpoints.listGames.useQuery( - { - ...options, - status: "Open", - }, - ); - return mergeQueries([listOpenGamesQuery], (games) => { - return games - }); -} - -export { useFindGames }; \ No newline at end of file diff --git a/src/screens/home/my-games/my-games.tsx b/src/screens/home/my-games.tsx similarity index 78% rename from src/screens/home/my-games/my-games.tsx rename to src/screens/home/my-games.tsx index e3af7582..24cacca5 100644 --- a/src/screens/home/my-games/my-games.tsx +++ b/src/screens/home/my-games.tsx @@ -22,8 +22,51 @@ import { StopCircleOutlined as FinishedIcon, SportsMotorsports as DiplicityIcon, } from "@mui/icons-material"; -import { useMyGames } from "./use-my-games"; -import { QueryContainer } from "../../../components"; +import { QueryContainer } from "../../components"; + +import { mergeQueries, service } from "../../common"; + +const options = { my: true, mastered: false }; + +const useMyGames = () => { + const { endpoints } = service; + const listVariantsQuery = endpoints.listVariants.useQuery(undefined); + const listStagingGamesQuery = endpoints.listGames.useQuery({ + ...options, + status: "Staging", + }); + const listStartedGamesQuery = endpoints.listGames.useQuery({ + ...options, + status: "Started", + }); + const listFinishedGamesQuery = endpoints.listGames.useQuery({ + ...options, + status: "Finished", + }); + const query = mergeQueries( + [ + listVariantsQuery, + listStagingGamesQuery, + listStartedGamesQuery, + listFinishedGamesQuery, + ], + (variants, stagingGames, startedGames, finishedGames) => { + const getMapSvgUrl = (game: (typeof stagingGames)[number]) => { + const variant = variants.find( + (variant) => variant.Name === game.Variant + ); + return variant?.Links?.find((link) => link.Rel === "map")?.URL; + }; + return { + getMapSvgUrl, + stagingGames, + startedGames, + finishedGames, + }; + } + ); + return { query }; +}; const StyledTabs = styled((props: React.ComponentProps) => ( { + const { query } = useMyGames(); const [selectedStatus, setSelectedStatus] = React.useState< Status | undefined >(undefined); - const query = useMyGames(); - const status = query.data ? selectedStatus ? selectedStatus diff --git a/src/screens/home/my-games/index.ts b/src/screens/home/my-games/index.ts deleted file mode 100644 index bb8289e0..00000000 --- a/src/screens/home/my-games/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./my-games" \ No newline at end of file diff --git a/src/screens/home/my-games/use-my-games.ts b/src/screens/home/my-games/use-my-games.ts deleted file mode 100644 index ad833a9b..00000000 --- a/src/screens/home/my-games/use-my-games.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { mergeQueries, service } from "../../../common"; - -const options = { my: true, mastered: false }; - -const useMyGames = () => { - const { endpoints } = service - const listVariantsQuery = endpoints.listVariants.useQuery(undefined); - const listStagingGamesQuery = endpoints.listGames.useQuery( - { - ...options, - status: "Staging", - }, - ); - const listStartedGamesQuery = endpoints.listGames.useQuery({ - ...options, - status: "Started", - }); - const listFinishedGamesQuery = endpoints.listGames.useQuery({ - ...options, - status: "Finished", - }); - return mergeQueries([listVariantsQuery, listStagingGamesQuery, listStartedGamesQuery, listFinishedGamesQuery], (variants, stagingGames, startedGames, finishedGames) => { - const getMapSvgUrl = (game: typeof stagingGames[number]) => { - const variant = variants.find((variant) => variant.Name === game.Variant); - return variant?.Links?.find((link) => link.Rel === "map")?.URL - } - return { - getMapSvgUrl, - stagingGames, - startedGames, - finishedGames, - } - }); -} - -export { useMyGames }; \ No newline at end of file diff --git a/src/screens/home/profile/profile.tsx b/src/screens/home/profile.tsx similarity index 80% rename from src/screens/home/profile/profile.tsx rename to src/screens/home/profile.tsx index 8057936a..3cd7294b 100644 --- a/src/screens/home/profile/profile.tsx +++ b/src/screens/home/profile.tsx @@ -11,19 +11,24 @@ import { Typography, } from "@mui/material"; import { MoreHoriz } from "@mui/icons-material"; -import { useProfile } from "./use-profile"; -import { QueryContainer } from "../../../components"; -import { AppDispatch } from "../../../common"; +import { QueryContainer } from "../../components"; +import { actions, AppDispatch, service } from "../../common"; import { useDispatch } from "react-redux"; -import { authActions } from "../../../common/store/auth"; -import { ScreenTopBar } from "../screen-title"; - -const Profile: React.FC = () => { - const query = useProfile(); +import { ScreenTopBar } from "./screen-top-bar"; +const useProfile = () => { const dispatch = useDispatch(); + const query = service.endpoints.getRoot.useQuery(undefined); + const handleLogout = () => { + dispatch(actions.logout()); + }; + return { query, handleLogout }; +}; +const Profile: React.FC = () => { + const { query, handleLogout } = useProfile(); const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); const handleMenuClick = (event: React.MouseEvent) => { @@ -41,10 +46,6 @@ const Profile: React.FC = () => { }; }; - const onClickLogout = () => { - dispatch(authActions.logout()); - }; - return ( <> @@ -71,7 +72,7 @@ const Profile: React.FC = () => { open={open} onClose={handleMenuClose} > - + Logout diff --git a/src/screens/home/profile/index.ts b/src/screens/home/profile/index.ts deleted file mode 100644 index 5300370a..00000000 --- a/src/screens/home/profile/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./profile"; \ No newline at end of file diff --git a/src/screens/home/profile/use-profile.ts b/src/screens/home/profile/use-profile.ts deleted file mode 100644 index c9b90c69..00000000 --- a/src/screens/home/profile/use-profile.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { service } from "../../../common"; - -const useProfile = () => { - const rootQuery = service.endpoints.getRoot.useQuery(undefined); - return rootQuery; -} - -export { useProfile }; \ No newline at end of file diff --git a/src/screens/home/screen-title.tsx b/src/screens/home/screen-top-bar.tsx similarity index 100% rename from src/screens/home/screen-title.tsx rename to src/screens/home/screen-top-bar.tsx From 43f9e3633119a87a9d48a5ed512d0649390dbcc8 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Mon, 27 Jan 2025 20:42:25 +0000 Subject: [PATCH 11/64] wip --- public/otto.png | Bin 0 -> 220229 bytes .../drawer-navigation/drawer-navigation.tsx | 36 ++- src/global.d.ts | 5 + src/screens/home/layout.tsx | 188 ++++++------- src/screens/home/my-games.tsx | 265 ++++++++++-------- src/screens/home/profile.tsx | 59 ++-- src/screens/home/screen-top-bar.tsx | 20 +- src/theme.ts | 9 + 8 files changed, 299 insertions(+), 283 deletions(-) create mode 100644 public/otto.png create mode 100644 src/global.d.ts diff --git a/public/otto.png b/public/otto.png new file mode 100644 index 0000000000000000000000000000000000000000..7cb5ff2e2290f9746beaab6d2d2d33259a06d146 GIT binary patch literal 220229 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 literal 0 HcmV?d00001 diff --git a/src/components/drawer-navigation/drawer-navigation.tsx b/src/components/drawer-navigation/drawer-navigation.tsx index 792c9028..c3c0af84 100644 --- a/src/components/drawer-navigation/drawer-navigation.tsx +++ b/src/components/drawer-navigation/drawer-navigation.tsx @@ -6,15 +6,15 @@ import { ListItemButton, ListItemIcon, ListItemText, + Stack, useTheme, } from "@mui/material"; -const drawerWidth = 240; - const DrawerNavigationContext = React.createContext< | { value: string; onChange: (newValue: string) => void; + variant: "expanded" | "collapsed"; } | undefined >(undefined); @@ -22,9 +22,11 @@ const DrawerNavigationContext = React.createContext< type DrawerNavigationProps = React.PropsWithChildren<{ value: string; onChange: (newValue: string) => void; + variant: "expanded" | "collapsed"; }>; const DrawerNavigation: React.FC = (props) => { + const drawerWidth = props.variant === "expanded" ? 240 : 56; return ( = (props) => { width: drawerWidth, flexShrink: 0, "& .MuiDrawer-paper": { - position: "relative", + position: props.variant === "expanded" ? "relative" : "fixed", width: drawerWidth, boxSizing: "border-box", border: "none", @@ -43,6 +45,7 @@ const DrawerNavigation: React.FC = (props) => { value={{ value: props.value, onChange: props.onChange, + variant: props.variant, }} > {props.children} @@ -83,17 +86,22 @@ const DrawerNavigationAction: React.FC = ( return ( context.onChange(props.value)}> - - {React.cloneElement(props.icon, { - style: selected ? selectedItemIconStyle : {}, - })} - - + + + {React.cloneElement(props.icon, { + style: selected ? selectedItemIconStyle : {}, + })} + + {context.variant === "expanded" && ( + + )} + ); diff --git a/src/global.d.ts b/src/global.d.ts new file mode 100644 index 00000000..9e9240a8 --- /dev/null +++ b/src/global.d.ts @@ -0,0 +1,5 @@ +import { SxProps, Theme } from "@mui/material"; + +declare global { + type Styles = Record>; +} \ No newline at end of file diff --git a/src/screens/home/layout.tsx b/src/screens/home/layout.tsx index c09a425d..749c69d2 100644 --- a/src/screens/home/layout.tsx +++ b/src/screens/home/layout.tsx @@ -8,8 +8,7 @@ import { Grid2, Stack, Typography, - styled, - Box, + Link, } from "@mui/material"; import { Outlet, useLocation, useNavigate } from "react-router"; import { @@ -20,6 +19,49 @@ import { } from "@mui/icons-material"; import { DrawerNavigation, DrawerNavigationAction } from "../../components"; +const styles: Styles = { + mobileAppBar: { + top: "auto", + bottom: 0, + }, + drawerNavigationContainer: { + position: "sticky", + top: 0, + alignSelf: "flex-start", + overflow: "auto", + }, + largeRoot: { + alignItems: "center", + }, + largeContentContainer: { + display: "flex", + justifyContent: "center", + flexGrow: 1, + maxWidth: 1000, + width: "100%", + }, + contentContainer: (theme) => ({ + width: "100%", + maxWidth: 600, + minHeight: "100vh", + height: "100%", + border: `1px solid ${theme.palette.divider}`, + }), + infoContainer: { + width: 240, + position: "sticky", + padding: 2, + top: 0, + alignSelf: "flex-start", + height: "100vh", + overflow: "auto", + gap: 1, + }, + infoTitle: { + fontWeight: 600, + }, +}; + const NavigationItems = [ { label: "My Games", icon: , value: "/" }, { label: "Find Games", icon: , value: "/find-games" }, @@ -27,17 +69,41 @@ const NavigationItems = [ { label: "Profile", icon: , value: "/profile" }, ] as const; -const ScreenContainer = styled(Box)(({ theme }) => ({ - height: "100%", - border: `1px solid ${theme.palette.divider}`, - borderRadius: theme.shape.borderRadius, -})); +const InfoPanel: React.FC = () => { + const learnLink = + "https://diplicity.notion.site/Diplicity-FAQ-7b4e0a119eb54c69b80b411f14d43bb9"; + const discordLink = + "https://discord.com/channels/565625522407604254/697344626859704340"; + + return ( + + + Welcome to Diplicity! + + + If you're new to the game, read our{" "} + + FAQ + + . + + + To chat with the developers or meet other players, join our{" "} + + Discord community + + . + + + ); +}; const Layout: React.FC = () => { const navigate = useNavigate(); const location = useLocation(); const theme = useTheme(); const isMobile = useMediaQuery(theme.breakpoints.down("sm")); + const isDesktop = useMediaQuery(theme.breakpoints.up("md")); const [navigation, setNavigation] = useState(location.pathname); useEffect(() => { @@ -54,11 +120,7 @@ const Layout: React.FC = () => { {isMobile ? ( <> - + handleNavigationChange(newValue)} @@ -75,24 +137,11 @@ const Layout: React.FC = () => { ) : ( - - - {/* Drawer navigation */} - + + + @@ -106,85 +155,14 @@ const Layout: React.FC = () => { ))} - {/* Main content */} - - - + + + - - - - {/* About panel */} - - - - Welcome to Diplicity! - - - If you're new to the game, you can learn more about it{" "} - - here - - . - - - To chat with the developers or meet other players, join our{" "} - - Discord server - - . - - - We massively appreciate your support and feedback! If you have - any questions or suggestions, please let us know by sending an{" "} - - email - - . - + + {isDesktop && } )} diff --git a/src/screens/home/my-games.tsx b/src/screens/home/my-games.tsx index 24cacca5..0490e29e 100644 --- a/src/screens/home/my-games.tsx +++ b/src/screens/home/my-games.tsx @@ -1,7 +1,6 @@ import React from "react"; import { Avatar, - Box, Button, IconButton, Link, @@ -10,7 +9,6 @@ import { ListItemAvatar, ListItemText, Stack, - styled, Tab, Tabs, Typography, @@ -20,17 +18,56 @@ import { AddCircleOutline as StagingIcon, PlayCircleOutline as StartedIcon, StopCircleOutlined as FinishedIcon, - SportsMotorsports as DiplicityIcon, } from "@mui/icons-material"; import { QueryContainer } from "../../components"; - import { mergeQueries, service } from "../../common"; -const options = { my: true, mastered: false }; +const styles: Styles = { + header: (theme) => ({ + borderBottom: `1px solid ${theme.palette.divider}`, + alignItems: "center", + }), + headerIcon: { + fontSize: 48, + }, + listItem: (theme) => ({ + gap: 1, + borderBottom: `1px solid ${theme.palette.divider}`, + alignItems: "center", + }), + mapContainer: { + display: "flex", + width: 80, + }, + map: { + borderRadius: 2, + }, + secondaryContainer: { + gap: 1, + }, + rulesContainer: { + gap: 1, + flexDirection: "row", + }, + avatarStackButton: { + justifyContent: "flex-start", + width: "fit-content", + }, + avatarStackContainer: { + alignItems: "center", + }, + avatar: { + width: 24, + height: 24, + }, + noGamesText: { + textAlign: "center", + }, +}; const useMyGames = () => { + const options = { my: true, mastered: false }; const { endpoints } = service; - const listVariantsQuery = endpoints.listVariants.useQuery(undefined); const listStagingGamesQuery = endpoints.listGames.useQuery({ ...options, status: "Staging", @@ -44,21 +81,9 @@ const useMyGames = () => { status: "Finished", }); const query = mergeQueries( - [ - listVariantsQuery, - listStagingGamesQuery, - listStartedGamesQuery, - listFinishedGamesQuery, - ], - (variants, stagingGames, startedGames, finishedGames) => { - const getMapSvgUrl = (game: (typeof stagingGames)[number]) => { - const variant = variants.find( - (variant) => variant.Name === game.Variant - ); - return variant?.Links?.find((link) => link.Rel === "map")?.URL; - }; + [listStagingGamesQuery, listStartedGamesQuery, listFinishedGamesQuery], + (stagingGames, startedGames, finishedGames) => { return { - getMapSvgUrl, stagingGames, startedGames, finishedGames, @@ -68,24 +93,6 @@ const useMyGames = () => { return { query }; }; -const StyledTabs = styled((props: React.ComponentProps) => ( - }} - /> -))(({ theme }) => ({ - "& .MuiTabs-indicator": { - display: "flex", - justifyContent: "center", - backgroundColor: "transparent", - }, - "& .MuiTabs-indicatorSpan": { - maxWidth: 40, - width: "100%", - backgroundColor: theme.palette.primary.main, - }, -})); - const statuses = [ { value: "staging", label: "Staging", icon: }, { value: "started", label: "Started", icon: }, @@ -111,94 +118,106 @@ const MyGames: React.FC = () => { : undefined; return ( - - - - - - setSelectedStatus(value)} - variant="fullWidth" - sx={{ width: "100%" }} - > - {statuses.map((status) => ( - - ))} - - - - - - {(data) => { - const games = - status === "staging" - ? data.stagingGames - : status === "started" - ? data.startedGames - : data.finishedGames; + + + Diplicity + setSelectedStatus(value)} + variant="fullWidth" + sx={styles.tabs} + > + {statuses.map((status) => ( + + ))} + + + + + {(data) => { + const games = + status === "staging" + ? data.stagingGames + : status === "started" + ? data.startedGames + : data.finishedGames; - return games.map((game) => ( - - - - } - > - - {game.Variant} - - - {game.Desc} - + if (games.length === 0) { + return ( + + + You are not a member of any {status} games. + + + Go to "Find games" to join a game or click "Create game" to + start a new game. + + + Join our Discord server to find other players to play with. + + + ); + } + + return games.map((game) => ( + + + } - secondary={ - - - {game.Variant} - - {game.PhaseLengthMinutes} - - - - - } - /> - - )); - }} - - + + + } + /> + + )); + }} + + + ); }; diff --git a/src/screens/home/profile.tsx b/src/screens/home/profile.tsx index 3cd7294b..ab38a26c 100644 --- a/src/screens/home/profile.tsx +++ b/src/screens/home/profile.tsx @@ -1,13 +1,10 @@ import React, { useState } from "react"; import { Avatar, - Card, - CardContent, Grid2, IconButton, Menu, MenuItem, - Stack, Typography, } from "@mui/material"; import { MoreHoriz } from "@mui/icons-material"; @@ -16,6 +13,14 @@ import { actions, AppDispatch, service } from "../../common"; import { useDispatch } from "react-redux"; import { ScreenTopBar } from "./screen-top-bar"; +const styles: Styles = { + root: { + gap: 2, + p: 2, + alignItems: "center", + }, +}; + const useProfile = () => { const dispatch = useDispatch(); const query = service.endpoints.getRoot.useQuery(undefined); @@ -51,36 +56,24 @@ const Profile: React.FC = () => { {(data) => ( - - - - - - - - - - {data.Name} - - - - - - -

- - Logout - - - - -
- - + + + + + + {data.Name} + + + + + + + + Logout + + + + )}
diff --git a/src/screens/home/screen-top-bar.tsx b/src/screens/home/screen-top-bar.tsx index 8470b88d..1daa007f 100644 --- a/src/screens/home/screen-top-bar.tsx +++ b/src/screens/home/screen-top-bar.tsx @@ -6,6 +6,16 @@ type ScreenTopBarProps = { title: string; }; +const styles: Styles = { + root: (theme) => ({ + borderBottom: `1px solid ${theme.palette.divider}`, + }), + iconButton: (theme) => ({ + padding: 0, + color: theme.palette.text.primary, + }), +}; + const ScreenTopBar: React.FC = ({ title }) => { const navigate = useNavigate(); @@ -15,15 +25,9 @@ const ScreenTopBar: React.FC = ({ title }) => { direction="row" alignItems="center" padding={2} - sx={(theme) => ({ borderBottom: `1px solid ${theme.palette.divider}` })} + sx={styles.root} > - navigate("/")} - sx={(theme) => ({ - padding: 0, - color: theme.palette.text.primary, - })} - > + navigate("/")} sx={styles.iconButton}> {title} diff --git a/src/theme.ts b/src/theme.ts index f853c76d..279549b7 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -36,6 +36,15 @@ const theme = createTheme({ main: "#FDE2B5", }, }, + components: { + MuiTypography: { + styleOverrides: { + root: { + textAlign: "left", + }, + }, + }, + } }); export default theme; \ No newline at end of file From fc3b4518e26213ab32dda3cd00e0171331529665 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Mon, 27 Jan 2025 20:43:39 +0000 Subject: [PATCH 12/64] Add styles for tabs in My Games screen to ensure full-width display --- src/screens/home/my-games.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/screens/home/my-games.tsx b/src/screens/home/my-games.tsx index 0490e29e..0dd13cb8 100644 --- a/src/screens/home/my-games.tsx +++ b/src/screens/home/my-games.tsx @@ -63,6 +63,9 @@ const styles: Styles = { noGamesText: { textAlign: "center", }, + tabs: { + width: "100%", + }, }; const useMyGames = () => { From 15a31785acd259e3a47af7ffd663fe9ad3516e69 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Mon, 27 Jan 2025 20:51:42 +0000 Subject: [PATCH 13/64] Refactor Login screen layout; replace Card with Stack for improved structure and add logo image; update My Games image size --- src/screens/Login.tsx | 36 ++++++++++++++++++++++------------- src/screens/home/my-games.tsx | 2 +- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx index 3495c8d7..ea807627 100644 --- a/src/screens/Login.tsx +++ b/src/screens/Login.tsx @@ -1,5 +1,12 @@ import React from "react"; -import { Button, Card, CardContent, Typography, Box } from "@mui/material"; +import { + Button, + Card, + CardContent, + Typography, + Box, + Stack, +} from "@mui/material"; const getLoginUrl = (): string => { const redirectUrl = location.href; @@ -25,18 +32,21 @@ const Login: React.FC = () => { height="100vh" bgcolor="#f5f5f5" > - - - - Welcome to Diplicity! - - - - - - + + Diplicity Logo + + Welcome to Diplicity! + + + + + ); }; diff --git a/src/screens/home/my-games.tsx b/src/screens/home/my-games.tsx index 0dd13cb8..d1884454 100644 --- a/src/screens/home/my-games.tsx +++ b/src/screens/home/my-games.tsx @@ -126,7 +126,7 @@ const MyGames: React.FC = () => { Diplicity Date: Tue, 28 Jan 2025 11:30:26 +0000 Subject: [PATCH 14/64] Implement player info and game info --- src/Router.tsx | 44 ++---- src/common/hooks/useGetVariantQuery.ts | 1 + src/screens/Login.tsx | 9 +- src/screens/home/game-info.tsx | 199 +++++++++++++++++++++++++ src/screens/home/index.ts | 4 +- src/screens/home/my-games.tsx | 80 +++++++++- src/screens/home/player-info.tsx | 47 ++++++ 7 files changed, 343 insertions(+), 41 deletions(-) create mode 100644 src/screens/home/game-info.tsx create mode 100644 src/screens/home/player-info.tsx diff --git a/src/Router.tsx b/src/Router.tsx index 632e5309..19209f3d 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Navigate, Outlet, Route, Routes, useNavigate } from "react-router"; +import { Navigate, Route, Routes, useNavigate } from "react-router"; import Login from "./screens/Login"; import { useSelector } from "react-redux"; import { selectAuth } from "./common/store/auth"; @@ -7,17 +7,15 @@ import { Map } from "./components/Map"; import { GameDetailsLayout } from "./components/GameDetailsLayout"; import { GameDetailsNavigation } from "./components/GameDetailsNavigation"; import { Orders } from "./components/orders"; -import { CreateOrder } from "./components/CreateOrder"; -import { Modal } from "./components/Modal"; import { CreateOrderAction } from "./components/CreateOrderAction"; import { ConfirmOrdersAction } from "./components/ConfirmOrdersAction"; -import { PlayerInfo } from "./components/PlayerInfo"; -import { GameInfo } from "./components/GameInfo"; import { CreateGame, FindGames, + GameInfo, Layout as HomeLayout, MyGames, + PlayerInfo, Profile, } from "./screens"; @@ -33,13 +31,13 @@ const Router: React.FC = () => { return loggedIn ? ( - }> - }> - } /> - } /> - } /> - } /> - + }> + } /> + } /> + } /> + } /> + } /> + } /> { onClickCreateOrder={onClickCreateOrder} navigation={} actions={[, ]} - modals={[ - {() => }, - ]} + modals={ + [ + // {() => }, + ] + } /> } > @@ -69,18 +69,4 @@ const Router: React.FC = () => { ); }; -const HomeModals: React.FC = () => { - return ( - <> - - - {({ value }) => } - - - {({ value }) => } - - - ); -}; - export default Router; diff --git a/src/common/hooks/useGetVariantQuery.ts b/src/common/hooks/useGetVariantQuery.ts index a5d696c0..bcc4e18c 100644 --- a/src/common/hooks/useGetVariantQuery.ts +++ b/src/common/hooks/useGetVariantQuery.ts @@ -8,6 +8,7 @@ const useGetVariantQuery = (gameId: string) => { const getGameQuery = endpoints.getGame.useQuery(gameId); const mergedQuery = mergeQueries([listVariantsQuery, getGameQuery], (variants, game) => { + console.log(variants, game); const variant = variants.find((variant) => variant.Name === game.Variant); if (!variant) throw new Error("Variant not found"); return variant; diff --git a/src/screens/Login.tsx b/src/screens/Login.tsx index ea807627..474b76c8 100644 --- a/src/screens/Login.tsx +++ b/src/screens/Login.tsx @@ -1,12 +1,5 @@ import React from "react"; -import { - Button, - Card, - CardContent, - Typography, - Box, - Stack, -} from "@mui/material"; +import { Button, Typography, Box, Stack } from "@mui/material"; const getLoginUrl = (): string => { const redirectUrl = location.href; diff --git a/src/screens/home/game-info.tsx b/src/screens/home/game-info.tsx new file mode 100644 index 00000000..2cf64228 --- /dev/null +++ b/src/screens/home/game-info.tsx @@ -0,0 +1,199 @@ +import React from "react"; +import { + Avatar, + Button, + Divider, + List, + ListItem, + ListItemIcon, + ListItemText, + ListSubheader, + Stack, +} from "@mui/material"; +import { + Map as VariantIcon, + TimerOutlined as DeadlinesIcon, + Language as LanguageIcon, + People as PlayersIcon, + Flag as WinConditionIcon, + CalendarToday as StartYearIcon, + Person as AuthorIcon, +} from "@mui/icons-material"; +import { QueryContainer } from "../../components"; +import { mergeQueries, service, useGetVariantQuery } from "../../common"; +import { ScreenTopBar } from "./screen-top-bar"; +import { useParams } from "react-router"; + +const styles: Styles = { + listSubheader: (theme) => ({ + textAlign: "left", + color: theme.palette.text.primary, + }), + listItemIcon: (theme) => ({ + color: theme.palette.text.primary, + minWidth: "fit-content", + padding: 1, + }), + listItemPrimaryText: (theme) => ({ + color: theme.palette.text.primary, + }), + listItemSecondaryText: (theme) => ({ + color: theme.palette.text.secondary, + paddingRight: 1, + "& .MuiListItemText-primary": { + textAlign: "right", + }, + }), + avatarStackButton: { + justifyContent: "flex-start", + width: "fit-content", + }, + avatarStackContainer: { + alignItems: "center", + }, + avatar: { + width: 24, + height: 24, + }, +}; + +const useGameInfo = () => { + const { gameId } = useParams(); + if (!gameId) throw new Error("Game ID not found"); + + const getGameQuery = service.endpoints.getGame.useQuery(gameId); + const getVariantQuery = useGetVariantQuery(gameId); + + const query = mergeQueries( + [getGameQuery, getVariantQuery], + (game, variant) => { + return { + game, + variant, + }; + } + ); + + return { query }; +}; + +const GameInfo: React.FC = () => { + const { query } = useGameInfo(); + + const TableListItem: React.FC<{ + label: string; + value: string | undefined; + icon: React.ReactElement; + }> = ({ label, value, icon }) => { + return ( + + {icon} + + + + ); + }; + + return ( + <> + + + {(data) => ( + + + Game settings + + } + /> + } + /> + {data.game.NonMovementPhaseLengthMinutes && ( + } + /> + )} + + + Player settings + + {data.game.ChatLanguageISO639_1 && ( + } + /> + )} + + + {data.game.Members.map((member) => ( + + ))} + + + } + > + + + + + + + + Variant details + + + } + /> + } + /> + } + /> + + + + + + + + + )} + + + ); +}; + +export { GameInfo }; diff --git a/src/screens/home/index.ts b/src/screens/home/index.ts index e50caf31..090a72a4 100644 --- a/src/screens/home/index.ts +++ b/src/screens/home/index.ts @@ -2,4 +2,6 @@ export * from "./layout"; export * from "./find-games" export * from "./create-game" export * from "./profile" -export * from "./my-games"; \ No newline at end of file +export * from "./my-games"; +export * from "./game-info"; +export * from "./player-info"; \ No newline at end of file diff --git a/src/screens/home/my-games.tsx b/src/screens/home/my-games.tsx index d1884454..438a9b58 100644 --- a/src/screens/home/my-games.tsx +++ b/src/screens/home/my-games.tsx @@ -8,6 +8,8 @@ import { ListItem, ListItemAvatar, ListItemText, + Menu, + MenuItem, Stack, Tab, Tabs, @@ -18,9 +20,13 @@ import { AddCircleOutline as StagingIcon, PlayCircleOutline as StartedIcon, StopCircleOutlined as FinishedIcon, + Info as InfoIcon, + Person as PlayerInfoIcon, + Share as ShareIcon, } from "@mui/icons-material"; import { QueryContainer } from "../../components"; import { mergeQueries, service } from "../../common"; +import { useNavigate } from "react-router"; const styles: Styles = { header: (theme) => ({ @@ -106,10 +112,39 @@ type Status = (typeof statuses)[number]["value"]; const MyGames: React.FC = () => { const { query } = useMyGames(); + const navigate = useNavigate(); const [selectedStatus, setSelectedStatus] = React.useState< Status | undefined >(undefined); + const [anchorEls, setAnchorEls] = React.useState<{ + [key: string]: HTMLElement | null; + }>({}); + + const handleMenuOpen = + (gameId: string) => (event: React.MouseEvent) => { + setAnchorEls((prev) => ({ ...prev, [gameId]: event.currentTarget })); + }; + + const handleMenuClose = (gameId: string) => () => { + setAnchorEls((prev) => ({ ...prev, [gameId]: null })); + }; + + const handleClickGameInfo = (gameId: string) => { + console.log("gameId", gameId); + navigate(`/game-info/${gameId}`); + }; + + const handleClickPlayerInfo = (userId: string) => { + navigate(`/player-info/${userId}`); + }; + + const handleClickShare = (gameId: string) => { + navigator.clipboard.writeText( + `${window.location.origin}/game-info/${gameId}` + ); + }; + const status = query.data ? selectedStatus ? selectedStatus @@ -170,9 +205,48 @@ const MyGames: React.FC = () => { - - + <> + + + + + { + handleClickGameInfo(game.ID); + handleMenuClose(game.ID)(); + }} + > + + Game info + + { + handleClickPlayerInfo(game.ID); + handleMenuClose(game.ID)(); + }} + > + + Player info + + { + handleClickShare(game.ID); + handleMenuClose(game.ID)(); + }} + > + + Share + + + } > diff --git a/src/screens/home/player-info.tsx b/src/screens/home/player-info.tsx new file mode 100644 index 00000000..e75a93cb --- /dev/null +++ b/src/screens/home/player-info.tsx @@ -0,0 +1,47 @@ +import React from "react"; +import { + Avatar, + List, + ListItem, + ListItemAvatar, + ListItemText, +} from "@mui/material"; +import { QueryContainer } from "../../components"; +import { service } from "../../common"; +import { ScreenTopBar } from "./screen-top-bar"; +import { useParams } from "react-router"; + +const usePlayerInfo = () => { + const { gameId } = useParams(); + if (!gameId) throw new Error("Game ID not found"); + + const query = service.endpoints.getGame.useQuery(gameId); + + return { query }; +}; + +const PlayerInfo: React.FC = () => { + const { query } = usePlayerInfo(); + + return ( + <> + + + {(data) => ( + + {data.Members.map((member) => ( + + + + + + + ))} + + )} + + + ); +}; + +export { PlayerInfo }; From 86d4285165891d893f08ff7c2162536948d782a4 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Wed, 29 Jan 2025 10:22:21 +0000 Subject: [PATCH 15/64] Add Google Fonts and update theme typography to use 'Cabin' font --- index.html | 4 ++++ src/theme.ts | 1 + 2 files changed, 5 insertions(+) diff --git a/index.html b/index.html index 4cd0db3c..fc97603d 100644 --- a/index.html +++ b/index.html @@ -4,6 +4,10 @@ + + + Diplicity + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + background + Created with Sketch. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Marseilles + Portugal + Spain + N. Africa + Tunis + WEST MEDITERRANEAN + GOL + Gulf of Lyon + Gascony + MID ATLANTIC + Brest + Paris + Picardy + Burgundy + Belgium + Holland + Ruhr + Munich + Piedmont + Tyrolia + Tuscany + Venice + Rome + Naples + Tyrrhenian Sea + Apulia + Trieste + Albania + Kiel + Greece + TYS + ADRIATIC SEA + IONIAN SEA + Serbia + Vienna + Budapest + Rumania + Bulgaria + SC + EC + SC + NC + SC + NC + AEGEAN SEA + Constantinople + Smyrna + Syria + EAST MED + Armenia + Ankara + Sevastopol + BLACK SEA + Bohemia + Berlin + Silesia + Galicia + Ukraine + Warsaw + Prussia + LVN + Livonia + Moscow + Denmark + St. Petersburg + Finland + Sweden + Norway + BALTIC SEA + SKA + BARENTS SEA + BOT + Gulf of Bothnia + HEL + Helgoland + Bight + Wales + London + IRISH SEA + ENGLISHCHANNEL + Yorkshire + LVP + Liverpool + Clyde + Edinburgh + NTH + North Sea + N. ATLANTIC + NRG + Norwegian Sea + + + + diff --git a/src/common/map/map.parse.ts b/src/common/map/map.parse.ts new file mode 100644 index 00000000..0ec60cf9 --- /dev/null +++ b/src/common/map/map.parse.ts @@ -0,0 +1,247 @@ +import { Map, Point, Text, PathStyles } from "./map.parse.types"; + +const convertCssStringToObject = (css: string) => { + const regex = /([\w-]*)\s*:\s*([^;]*)/g; + let match; + const properties: Record = {}; + while ((match = regex.exec(css))) { + properties[match[1]] = match[2].trim(); + } + return properties; +}; + + +const recognizedTags = ["path", "polygon", "polyline", "rect"]; + +class SvgParser { + private svgString: string; + + constructor(svgString: string) { + console.log("Initialized with string: ", svgString); + this.svgString = svgString; + } + + public parse(): Map { + const parser = new DOMParser(); + const doc = parser.parseFromString(this.svgString, "image/svg+xml"); + const root = doc.documentElement; + + return { + height: this.getHeight(root), + width: this.getWidth(root), + backgroundElements: this.parseBackgroundElements(root), + borders: this.parseBorders(root), + provinces: this.parseProvinces(root), + impassableProvinces: this.parseImpassableProvinces(root) + }; + } + + private getHeight(root: Element): number { + console.log("Getting height for selector: #background-rect"); + const element = root.querySelector("#background-rect"); + console.log("Element: ", element); + return parseFloat(element?.getAttribute("height") || "0"); + } + + private getWidth(root: Element): number { + console.log("Getting width for selector: #background-rect"); + const element = root.querySelector("#background-rect"); + console.log("Element: ", element); + return parseFloat(element?.getAttribute("width") || "0"); + } + + private convertRectToPath(rect: Element): Element { + const x = parseFloat(rect.getAttribute("x") || "0"); + const y = parseFloat(rect.getAttribute("y") || "0"); + const width = parseFloat(rect.getAttribute("width") || "0"); + const height = parseFloat(rect.getAttribute("height") || "0"); + const path = `M${x} ${y} L${x + width} ${y} L${x + width} ${y + height} L${x} ${y + height} Z`; + const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path"); + pathElement.setAttribute("d", path); + pathElement.setAttribute("style", rect.getAttribute("style") || ""); + return pathElement; + } + + private convertPolygonToPath(polygon: Element): Element { + const points = polygon.getAttribute("points") || ""; + const pointsArray = points.trim().split(" "); + let pathData = `M ${pointsArray[0]}`; + for (let i = 1; i < pointsArray.length; i++) { + pathData += ` L ${pointsArray[i]}`; + } + pathData += " Z"; + const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path"); + pathElement.setAttribute("d", pathData); + pathElement.setAttribute("style", polygon.getAttribute("style") || ""); + return pathElement; + } + + private convertPolylineToPath(polyline: Element): Element { + const points = polyline.getAttribute("points") || ""; + const path = `M${points}`; + const pathElement = document.createElementNS("http://www.w3.org/2000/svg", "path"); + pathElement.setAttribute("d", path); + pathElement.setAttribute("style", polyline.getAttribute("style") || ""); + return pathElement; + } + + private parseBackgroundElements(root: Element): Map["backgroundElements"] { + console.log("Parsing background elements"); + const backgroundLayer = root.querySelector("#background"); + const elements = backgroundLayer?.querySelectorAll("path, rect"); + console.log("Elements: ", elements); + if (!elements) return []; + return Array.from(elements).map(element => { + if (element.tagName === "rect") { + const path = this.convertRectToPath(element); + return { + path: path.getAttribute("d") || "", + styles: this.parsePathStyles(path) + } + } + return { + path: element.getAttribute("d") || "", + styles: this.parsePathStyles(element) + } + }) + } + + private parseBorders(root: Element): Map["borders"] { + const foregroundLayer = root.querySelector("#foreground"); + const elements = foregroundLayer?.querySelectorAll("path, polygon, polyline"); + if (!elements) return []; + + // Only include elements that don't have fill:url(#impassableStripes) + const filteredElements = Array.from(elements).filter(element => { + const fill = convertCssStringToObject(element.getAttribute("style") || "").fill; + return fill !== "url(#impassableStripes)"; + }) + + // If element is polyline, return points and transform + return Array.from(filteredElements).map(element => { + if (element.tagName === "polygon" || element.tagName === "polyline") { + const path = element.tagName === "polygon" ? this.convertPolygonToPath(element) : this.convertPolylineToPath(element); + return { + path: path.getAttribute("d") || "", + } + } + return { + path: element.getAttribute("d") || "" + } + }) + } + + private parseImpassableProvinces(root: Element): Map["impassableProvinces"] { + const foregroundLayer = root.querySelector("#foreground"); + const elements = foregroundLayer?.querySelectorAll("path, polygon, polyline"); + if (!elements) return []; + + // Only include elements that don't have fill:url(#impassableStripes) + const filteredElements = Array.from(elements).filter(element => { + const fill = convertCssStringToObject(element.getAttribute("style") || "").fill; + return fill === "url(#impassableStripes)"; + }) + + // If element is polyline, return points and transform + return Array.from(filteredElements).map(element => { + if (element.tagName === "polygon" || element.tagName === "polyline") { + const path = element.tagName === "polygon" ? this.convertPolygonToPath(element) : this.convertPolylineToPath(element); + return { + path: path.getAttribute("d") || "", + } + } + return { + path: element.getAttribute("d") || "" + } + }) + } + + private parseProvinces(root: Element): Map["provinces"] { + console.log("Parsing provinces elements"); + const provincesLayer = root.querySelector("#provinces"); + const supplyCentersLayer = root.querySelector("#supply-centers"); + const provinceCentersLayer = root.querySelector("#province-centers"); + const namesLayer = root.querySelector("#names"); + const elements = provincesLayer?.querySelectorAll(recognizedTags.join(", ")); + if (!elements) return []; + return Array.from(elements).map(element => { + const escapedId = element.id.replace("/", "\\/"); + const supplyCenter = supplyCentersLayer?.querySelector(`#${escapedId}Center`); + const provinceCenter = provinceCentersLayer?.querySelector(`#${escapedId}Center`); + const [x, y] = supplyCenter ? this.parseDCenter(supplyCenter as HTMLElement) : provinceCenter ? this.parseDCenter(provinceCenter as HTMLElement) : [0, 0]; + + if (element.tagName === "polygon") { + const path = this.convertPolygonToPath(element); + return { + id: element.id, + center: { x, y }, + supplyCenter: Boolean(supplyCenter), + path: path.getAttribute("d") || "", + text: this.parseText(namesLayer?.querySelector(`#${escapedId}`) || null), + } + } else { + + return { + id: element.id, + center: { x, y }, + supplyCenter: !!supplyCenter, + path: element.getAttribute("d") || "", + text: this.parseText(namesLayer?.querySelector(`#${escapedId}`) || null), + } + } + }); + } + + private parseDCenter(element: HTMLElement): [number, number] { + const d = element.getAttribute("d") || ""; + const match = /^m\s+([\d-.]+),([\d-.]+)\s+/.exec(d); + if (!match) throw new Error(`Invalid d attribute: ${d}`); + return [Number(match[1]), Number(match[2])]; + } + + private parsePoint(element: Element): Point { + return { + x: parseFloat(element.getAttribute("x") || "0"), + y: parseFloat(element.getAttribute("y") || "0") + }; + } + + private parseText(element: Element | null): Text | undefined { + if (!element) return undefined + const tspan = element.querySelector("tspan"); + if (!tspan) return undefined; + const cssStyles = convertCssStringToObject(element.getAttribute("style") || "") + const textPoint = this.parsePoint(tspan); + const textOffset = this.parsePoint(element); + const x = textPoint.x - textOffset.x; + const y = textPoint.y - textOffset.y; + return { + value: element.textContent || "", + styles: { + fontSize: cssStyles["font-size"] || "", + fontFamily: cssStyles["font-family"] || "", + fontWeight: cssStyles["font-weight"] || "", + transform: cssStyles.transform || "" + }, + point: { x, y } + }; + } + + private parsePathStyles(element: Element): PathStyles { + const cssStyles = convertCssStringToObject(element.getAttribute("style") || "") + return { + fill: cssStyles.fill || "", + stroke: cssStyles.stroke, + strokeDasharray: cssStyles["stroke-dasharray"], + strokeMiterlimit: cssStyles["stroke-miterlimit"], + strokeOpacity: cssStyles["stroke-opacity"], + strokeWidth: cssStyles["stroke-width"], + } + } +} + +const parseSvg = (svgString: string): Map => { + return new SvgParser(svgString).parse(); +}; + +export { parseSvg }; \ No newline at end of file diff --git a/src/common/map/map.parse.types.ts b/src/common/map/map.parse.types.ts new file mode 100644 index 00000000..e04c67a5 --- /dev/null +++ b/src/common/map/map.parse.types.ts @@ -0,0 +1,52 @@ +import { CSSProperties } from "react"; + +type Point = { + x: number; + y: number; +} + +type Path = string; + +type TextStyles = Pick; + +type PathStyles = Pick; + +type Text = { + value: string; + styles: TextStyles; + point: Point; +} + +type Province = { + id: string; + center: Point; + supplyCenter: boolean; + text: Text | undefined; + path: string; + transform?: string; +} + +type ImpassableProvince = { + path: string; +} + +type Border = { + path: string; +} + + +type BackgroundElement = { + path: Path; + styles: PathStyles; +} + +type Map = { + height: number; + width: number; + backgroundElements: BackgroundElement[]; + borders: Border[]; + provinces: Province[]; + impassableProvinces: ImpassableProvince[]; +} + +export type { Map, Point, Text, PathStyles } \ No newline at end of file diff --git a/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx b/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx deleted file mode 100644 index d2aeeb50..00000000 --- a/src/components/ConfirmOrdersAction/ConfirmOrdersAction.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Fab, Stack, Typography } from "@mui/material"; -import { - CheckBox as OrdersConfirmedIcon, - CheckBoxOutlineBlank as ConfirmOrdersIcon, -} from "@mui/icons-material"; -import { - mergeQueries, - useGetUserNewestPhaseStateQuery, - useUpdatePhaseStateMutation, -} from "../../common"; -import { useGameDetailContext } from "../../context"; - -const useConfirmOrdersAction = () => { - const { gameId } = useGameDetailContext(); - const phaseStateQuery = useGetUserNewestPhaseStateQuery(gameId); - const [updatePhaseStateTrigger, updatePhaseStateMutation] = - useUpdatePhaseStateMutation(gameId); - - const handleClick = () => - updatePhaseStateTrigger({ - isConfirmed: !phaseStateQuery.data?.ReadyToResolve, - }); - - const mergedQuery = mergeQueries([phaseStateQuery], (phaseState) => ({ - isConfirmed: Boolean(phaseState?.ReadyToResolve), - canUpdate: Boolean(phaseState?.canUpdate), - })); - - return { - ...mergedQuery, - isSubmitting: updatePhaseStateMutation.isLoading, - handleClick, - }; -}; - -const ConfirmOrdersAction: React.FC = () => { - const { isLoading, isError, isSuccess, isSubmitting, data, handleClick } = - useConfirmOrdersAction(); - - if (isLoading) return <>; - if (isError) return <>; - if (!isSuccess) throw new Error("Something went wrong"); - if (!data.canUpdate) return <>; - - return ( - - - {data.isConfirmed ? : } - {data.isConfirmed ? ( - Orders Confirmed - ) : ( - Confirm Orders - )} - - - ); -}; - -export { ConfirmOrdersAction }; diff --git a/src/components/ConfirmOrdersAction/index.ts b/src/components/ConfirmOrdersAction/index.ts deleted file mode 100644 index 2406e770..00000000 --- a/src/components/ConfirmOrdersAction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./ConfirmOrdersAction"; \ No newline at end of file diff --git a/src/components/CreateOrderAction/CreateOrderAction.hook.ts b/src/components/CreateOrderAction/CreateOrderAction.hook.ts deleted file mode 100644 index 102ae35d..00000000 --- a/src/components/CreateOrderAction/CreateOrderAction.hook.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { useLocation, useNavigate } from "react-router"; -import { mergeQueries, useGetCurrentPhaseQuery } from "../../common"; -import { useGameDetailContext } from "../../context"; - -const useCreateOrderAction = () => { - const { gameId } = useGameDetailContext(); - const location = useLocation(); - const navigate = useNavigate(); - - const currentPhaseQuery = useGetCurrentPhaseQuery(gameId); - - const handleClick = () => { - const searchParams = new URLSearchParams(location.search); - searchParams.set("createOrder", "true"); - navigate({ search: searchParams.toString() }); - }; - - const mergedQuery = mergeQueries([currentPhaseQuery], (phase) => ({ - canCreateOrder: Boolean(phase.canCreateOrder), - })); - - return { - ...mergedQuery, - handleClick, - }; -}; - -export { useCreateOrderAction }; \ No newline at end of file diff --git a/src/components/CreateOrderAction/CreateOrderAction.tsx b/src/components/CreateOrderAction/CreateOrderAction.tsx deleted file mode 100644 index 844248cd..00000000 --- a/src/components/CreateOrderAction/CreateOrderAction.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { Fab } from "@mui/material"; -import { Add as CreateOrderIcon } from "@mui/icons-material"; -import { useCreateOrderAction } from "./CreateOrderAction.hook"; - -const CreateOrderAction: React.FC = () => { - const { isLoading, isError, data, handleClick } = useCreateOrderAction(); - - if (isLoading) return <>; - if (isError) return <>; - if (!data?.canCreateOrder) return <>; - - return ( - - - - ); -}; - -export { CreateOrderAction }; diff --git a/src/components/CreateOrderAction/index.ts b/src/components/CreateOrderAction/index.ts deleted file mode 100644 index b0b4d9f8..00000000 --- a/src/components/CreateOrderAction/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./CreateOrderAction" \ No newline at end of file diff --git a/src/components/GameDetailsLayout/GameDetailsLayout.tsx b/src/components/GameDetailsLayout/GameDetailsLayout.tsx deleted file mode 100644 index 8354ccca..00000000 --- a/src/components/GameDetailsLayout/GameDetailsLayout.tsx +++ /dev/null @@ -1,65 +0,0 @@ -import { Outlet } from "react-router"; -import { AppBar, IconButton, Stack, Toolbar, useTheme } from "@mui/material"; -import { ArrowBack as BackIcon } from "@mui/icons-material"; -import { PhaseSelect } from "../phase-select"; -import { - GameDetailContextProvider, - SelectedPhaseContextProvider, -} from "../../context"; - -const GameDetailsLayout: React.FC<{ - onClickBack: () => void; - onClickCreateOrder: () => void; - actions: React.ReactNode[]; - navigation: React.ReactNode; - modals: React.ReactNode[]; -}> = (props) => { - const theme = useTheme(); - - return ( - - - - - - - - - - - - - - - {props.actions} - - {props.navigation} - {props.modals} - - - - ); -}; - -export { GameDetailsLayout }; diff --git a/src/components/GameDetailsLayout/index.ts b/src/components/GameDetailsLayout/index.ts deleted file mode 100644 index 3383f5db..00000000 --- a/src/components/GameDetailsLayout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./GameDetailsLayout" \ No newline at end of file diff --git a/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx b/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx deleted file mode 100644 index e48161d1..00000000 --- a/src/components/GameDetailsNavigation/GameDetailsNavigation.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import { - AppBar, - BottomNavigation, - BottomNavigationAction, -} from "@mui/material"; -import { - Map as MapIcon, - Gavel as OrdersIcon, - People as PlayersIcon, -} from "@mui/icons-material"; -import { useLocation, useNavigate } from "react-router"; -import { useGameDetailContext } from "../../context"; - -const GameDetailsNavigation: React.FC = () => { - const { gameId } = useGameDetailContext(); - const location = useLocation(); - const navigate = useNavigate(); - - const navigationPathMap = { - map: `/game/${gameId}`, - orders: `/game/${gameId}/orders`, - players: `/game/${gameId}/players`, - } as const; - - return ( - - { - navigate(value); - }} - > - } - value={navigationPathMap.map} - /> - } - value={navigationPathMap.orders} - /> - } - value={navigationPathMap.players} - /> - - - ); -}; - -export { GameDetailsNavigation }; diff --git a/src/components/GameDetailsNavigation/index.ts b/src/components/GameDetailsNavigation/index.ts deleted file mode 100644 index 5acc94f7..00000000 --- a/src/components/GameDetailsNavigation/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./GameDetailsNavigation" \ No newline at end of file diff --git a/src/components/InteractiveMap/InteractiveMap.stories.tsx b/src/components/InteractiveMap/InteractiveMap.stories.tsx new file mode 100644 index 00000000..2f6ce2ad --- /dev/null +++ b/src/components/InteractiveMap/InteractiveMap.stories.tsx @@ -0,0 +1,15 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { InteractiveMap } from "./InteractiveMap"; +import classical from "../../data/map/classical.json"; + +export default { + title: "Components/InteractiveMap", + component: InteractiveMap, + args: { + map: classical, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/InteractiveMap/InteractiveMap.tsx b/src/components/InteractiveMap/InteractiveMap.tsx new file mode 100644 index 00000000..a14214ef --- /dev/null +++ b/src/components/InteractiveMap/InteractiveMap.tsx @@ -0,0 +1,143 @@ +import React, { useState } from "react"; +import { Map } from "../../common/map/map.parse.types"; + +type InteractiveMapProps = { + map: Map; +}; + +const HOVER_STROKE_WIDTH = 1; +const HOVER_STROKE_COLOR = "white"; +const HOVER_FILL = "rgba(255, 255, 255, 0.6)"; + +const SELECTED_STROKE_WIDTH = 3; +const SELECTED_STROKE_COLOR = "white"; +const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; + +const DEFAULT_FILL = "transparent"; + +const InteractiveMap: React.FC = ({ map }) => { + const [hoveredProvince, setHoveredProvince] = useState(null); + const [selectedProvince, setSelectedProvince] = useState(null); + + const getFill = (provinceId: string) => { + if (selectedProvince === provinceId) return SELECTED_FILL; + if (hoveredProvince === provinceId) return HOVER_FILL; + return DEFAULT_FILL; + }; + + const getStroke = (provinceId: string) => { + if (selectedProvince === provinceId) return SELECTED_STROKE_COLOR; + if (hoveredProvince === provinceId) return HOVER_STROKE_COLOR; + return "none"; + }; + + const getStrokeWidth = (provinceId: string) => { + if (selectedProvince === provinceId) return SELECTED_STROKE_WIDTH; + if (hoveredProvince === provinceId) return HOVER_STROKE_WIDTH; + return 1; + }; + + return ( + + + + + + + {map.backgroundElements.map((element, index) => ( + + + + ))} + {map.provinces.map((province) => ( + + setHoveredProvince(province.id)} + onMouseLeave={() => setHoveredProvince(null)} + onClick={() => setSelectedProvince(province.id)} + /> + {province.supplyCenter && ( + + + + + )} + + ))} + {map.provinces.map( + (province) => + province.text && ( + + {province.text.value} + + ) + )} + {map.borders.map((element, index) => ( + + ))} + {map.impassableProvinces.map((element, index) => ( + + ))} + + ); +}; + +export { InteractiveMap }; diff --git a/src/components/Map/Map.tsx b/src/components/Map/Map.tsx index 886cb66f..9df9a440 100644 --- a/src/components/Map/Map.tsx +++ b/src/components/Map/Map.tsx @@ -1,43 +1,20 @@ -import React from "react"; -import { CircularProgress, Box } from "@mui/material"; -import { useMap } from "./use-map"; - -const MapContainer: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - return ( - - {children} - - ); -}; +import { useMap } from "../../common"; +import { QueryContainer } from "../query-container"; const Map: React.FC = () => { - const { isLoading, isError, isSuccess, data } = useMap(); - - if (isLoading) { - return ( - - - - ); - } - - if (isError) return
Error loading map
; - if (!isSuccess) return null; - + const { query } = useMap(); return ( - -
- + + {(data) => ( +
+ )} + ); }; diff --git a/src/components/Map/index.ts b/src/components/Map/index.ts index a9fddd5c..fc155f27 100644 --- a/src/components/Map/index.ts +++ b/src/components/Map/index.ts @@ -1 +1 @@ -export * from "./Map"; \ No newline at end of file +export * from "./map"; \ No newline at end of file diff --git a/src/components/Map/use-map.ts b/src/components/Map/use-map.ts deleted file mode 100644 index 55b7956d..00000000 --- a/src/components/Map/use-map.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { createMap } from "../../common/map/map"; -import { mergeQueries, useGetPhaseQuery, useGetMapSvgQuery, useGetUnitSvgQuery, useGetVariantQuery } from "../../common"; -import { useGameDetailContext, useSelectedPhaseContext } from "../../context"; - -const useMap = () => { - const { gameId } = useGameDetailContext(); - const { selectedPhase } = useSelectedPhaseContext(); - - const getVariantQuery = useGetVariantQuery(gameId); - const getPhaseQuery = useGetPhaseQuery(gameId, selectedPhase); - const getMapSvgQuery = useGetMapSvgQuery(gameId); - const getArmySvgQuery = useGetUnitSvgQuery(gameId, "Army"); - const getFleetSvgQuery = useGetUnitSvgQuery(gameId, "Fleet"); - - return mergeQueries([getVariantQuery, getPhaseQuery, getMapSvgQuery, getArmySvgQuery, getFleetSvgQuery], (variant, phase, mapSvg, armySvg, fleetSvg) => { - return createMap(mapSvg, armySvg, fleetSvg, variant, phase); - }); -} - -export { useMap }; diff --git a/src/components/Modal/Modal.context.ts b/src/components/Modal/Modal.context.ts deleted file mode 100644 index 80f07d2a..00000000 --- a/src/components/Modal/Modal.context.ts +++ /dev/null @@ -1,8 +0,0 @@ -import React from "react"; -import { ModalContextType } from "./Modal.types"; - -const ModalContext = React.createContext( - undefined -); - -export { ModalContext }; \ No newline at end of file diff --git a/src/components/Modal/Modal.hook.ts b/src/components/Modal/Modal.hook.ts deleted file mode 100644 index eefe6cd8..00000000 --- a/src/components/Modal/Modal.hook.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { useContext } from "react" -import { ModalContext } from "./Modal.context" - -const useModal = () => { - const context = useContext(ModalContext) - if (!context) { - throw new Error("useModal must be used within a ModalProvider") - } - return context -} - -export { useModal } \ No newline at end of file diff --git a/src/components/Modal/Modal.tsx b/src/components/Modal/Modal.tsx deleted file mode 100644 index 5cc33e96..00000000 --- a/src/components/Modal/Modal.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import React from "react"; -import { Modal as MuiModal, Box, Stack } from "@mui/material"; -import { useLocation, useNavigate } from "react-router"; -import { ModalContext } from "./Modal.context"; - -const style = { - position: "absolute", - top: "50%", - left: "50%", - transform: "translate(-50%, -50%)", - width: 400, - bgcolor: "background.paper", - boxShadow: 24, - p: 4, -}; - -const Modal: React.FC<{ - name: string; - // Update children to be a render prop that passes the value and onClose function - // children: React.ReactNode; - children: (props: { - value: string | null; - onClose: () => void; - }) => React.ReactNode; -}> = (props) => { - const navigate = useNavigate(); - const location = useLocation(); - const queryParams = new URLSearchParams(location.search); - const isOpen = queryParams.has(props.name); - const value = queryParams.get(props.name); - - const onClose = () => { - const searchParams = new URLSearchParams(location.search); - searchParams.delete(props.name); - navigate({ search: searchParams.toString() }); - }; - - return ( - - - - - {props.children({ value, onClose })} - - - - - ); -}; - -export { Modal }; diff --git a/src/components/Modal/Modal.types.ts b/src/components/Modal/Modal.types.ts deleted file mode 100644 index 3f94b302..00000000 --- a/src/components/Modal/Modal.types.ts +++ /dev/null @@ -1,6 +0,0 @@ -type ModalContextType = { - onClose: () => void; - value: string | null; -}; - -export type { ModalContextType }; \ No newline at end of file diff --git a/src/components/Modal/index.ts b/src/components/Modal/index.ts deleted file mode 100644 index dde8dfc1..00000000 --- a/src/components/Modal/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./Modal"; -export * from "./Modal.hook"; \ No newline at end of file diff --git a/src/components/PlayerInfo.tsx b/src/components/PlayerInfo.tsx deleted file mode 100644 index 652ec4b7..00000000 --- a/src/components/PlayerInfo.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import React from "react"; -import { Stack, Typography } from "@mui/material"; -import { PlayerInfoCard } from "./PlayerInfoCard"; -import service from "../common/store/service"; - -const PlayerInfo: React.FC<{ - gameId: string; - getGameQuery?: typeof service.endpoints.getGame.useQuery; -}> = (props) => { - const getGameQuery = props.getGameQuery - ? props.getGameQuery(props.gameId) - : service.endpoints.getGame.useQuery(props.gameId); - - if (getGameQuery.isSuccess) { - return ( - - Player Info - - {getGameQuery.data.Members.map((member) => ( - - ))} - - - ); - } -}; - -export { PlayerInfo }; diff --git a/src/components/PlayerInfoCard.tsx b/src/components/PlayerInfoCard.tsx deleted file mode 100644 index 70865f91..00000000 --- a/src/components/PlayerInfoCard.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; -import { - Avatar, - Card, - CardContent, - Grid2 as Grid, - Typography, -} from "@mui/material"; -import service from "../common/store/service"; - -const PlayerInfoCard: React.FC<{ - member: (typeof service.endpoints.getGame.Types.ResultType)["Members"][0]; -}> = (props) => { - return ( - - - - - - - - - - - - - {props.member.User.Name} - - - - - - - - - - - ); -}; - -export { PlayerInfoCard }; diff --git a/src/components/UserInfo.tsx b/src/components/UserInfo.tsx deleted file mode 100644 index 1070a565..00000000 --- a/src/components/UserInfo.tsx +++ /dev/null @@ -1,101 +0,0 @@ -import React, { useState } from "react"; -import { - Card, - CardContent, - Grid2 as Grid, - IconButton, - Typography, - Menu, - MenuItem, - Avatar, - Stack, - Skeleton, -} from "@mui/material"; -import { MoreHoriz } from "@mui/icons-material"; -import service from "../common/store/service"; -import { useDispatch } from "react-redux"; -import { AppDispatch } from "../common"; -import { authActions } from "../common/store/auth"; - -const UserInfo: React.FC = () => { - const rootQuery = service.endpoints.getRoot.useQuery(undefined); - const dispatch = useDispatch(); - - const [anchorEl, setAnchorEl] = useState(null); - const open = Boolean(anchorEl); - - const handleMenuClick = (event: React.MouseEvent) => { - setAnchorEl(event.currentTarget); - }; - - const handleMenuClose = () => { - setAnchorEl(null); - }; - - const withMenuClose = (fn: () => void) => { - return () => { - fn(); - handleMenuClose(); - }; - }; - - const onClickLogout = () => { - dispatch(authActions.logout()); - }; - - if (rootQuery.isLoading) { - return ( - - - - - - - - - - - - - - - - - - - ); - } - - const user = rootQuery.data; - - return ( - - - - - - - - - - {user?.Name} - - - - - - - - - Logout - - - - - - - - ); -}; - -export default UserInfo; diff --git a/src/screens/game-detail/channel-list.tsx b/src/components/channel/channel-list.tsx similarity index 100% rename from src/screens/game-detail/channel-list.tsx rename to src/components/channel/channel-list.tsx diff --git a/src/screens/game-detail/channel.tsx b/src/components/channel/channel.tsx similarity index 57% rename from src/screens/game-detail/channel.tsx rename to src/components/channel/channel.tsx index 55989869..58392b2e 100644 --- a/src/screens/game-detail/channel.tsx +++ b/src/components/channel/channel.tsx @@ -1,6 +1,4 @@ import React, { useEffect, useRef } from "react"; -import { ScreenTopBar } from "../home/screen-top-bar"; -import { useGameDetailContext } from "../../context"; import { mergeQueries, service, useGetVariantQuery } from "../../common"; import { useChannelContext } from "../../context/channel-context"; import { @@ -14,14 +12,13 @@ import { Stack, TextField, Typography, - useMediaQuery, - useTheme, } from "@mui/material"; import { Send as SendIcon } from "@mui/icons-material"; import { QueryContainer } from "../../components"; import { useGetUserMemberQuery } from "../../common/hooks/useGetUserMemberQuery"; import { useGetChannelQuery } from "../../common/hooks/useGetChannelQuery"; import { getChannelDisplayName } from "../../util"; +import { useGameDetailContext } from "../../context"; const styles: Styles = { listSubheader: (theme) => ({ @@ -128,12 +125,8 @@ const displayTime = (date: Date) => { }); }; -const ChannelComponent: React.FC = () => { - const { query, message, setMessage, handleSubmit, isSubmitting, closed } = - useChannel(); - - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("md")); +const Channel: React.FC = () => { + const { query } = useChannel(); const listRef = useRef(null); @@ -144,80 +137,78 @@ const ChannelComponent: React.FC = () => { }, [query.data]); return ( - <> - {isMobile && } - - {(data) => ( - - - - {Object.keys(data.messages).map((date) => ( - - - {date} - - {data.messages[date].map((message, index) => ( - - - {message.sender[0]} - - - {message.body} - - {displayTime(new Date(message.date))} - - - } - /> - - ))} - - ))} - - - {!closed && ( - - setMessage(e.target.value)} - fullWidth - disabled={isSubmitting} - /> - - - - - )} + + {(data) => ( + + + + {Object.keys(data.messages).map((date) => ( + + + {date} + + {data.messages[date].map((message, index) => ( + + + {message.sender[0]} + + + {message.body} + + {displayTime(new Date(message.date))} + + + } + /> + + ))} + + ))} + - )} - - + + )} + ); }; -const Channel: React.FC = () => { - return ; +const ChannelTextField: React.FC = () => { + const { message, setMessage, handleSubmit, isSubmitting, closed } = + useChannel(); + + return closed ? null : ( + + setMessage(e.target.value)} + fullWidth + disabled={isSubmitting} + /> + + + + + ); }; -export { Channel }; +export { Channel, ChannelTextField }; diff --git a/src/components/channel/create-channel.tsx b/src/components/channel/create-channel.tsx new file mode 100644 index 00000000..88681d7e --- /dev/null +++ b/src/components/channel/create-channel.tsx @@ -0,0 +1,5 @@ +const CreateChannel: React.FC = () => { + return
Hello world
; +}; + +export { CreateChannel }; diff --git a/src/components/channel/index.ts b/src/components/channel/index.ts new file mode 100644 index 00000000..1d91067d --- /dev/null +++ b/src/components/channel/index.ts @@ -0,0 +1,3 @@ +export * from "./channel"; +export * from "./channel-list"; +export * from "./create-channel"; \ No newline at end of file diff --git a/src/components/game-detail/game-name.tsx b/src/components/game-detail/game-name.tsx new file mode 100644 index 00000000..d3dfeaeb --- /dev/null +++ b/src/components/game-detail/game-name.tsx @@ -0,0 +1,17 @@ +import { Typography } from "@mui/material"; +import { service } from "../../common"; +import { useGameDetailContext } from "../../context"; +import { QueryContainer } from "../query-container"; + +const GameName: React.FC = () => { + const { gameId } = useGameDetailContext(); + const getGameQuery = service.endpoints.getGame.useQuery(gameId); + + return ( + <>}> + {(data) => {data.Desc}} + + ); +}; + +export { GameName }; diff --git a/src/components/home-layout/home-layout.tsx b/src/components/home-layout/home-layout.tsx deleted file mode 100644 index 4395fba0..00000000 --- a/src/components/home-layout/home-layout.tsx +++ /dev/null @@ -1,262 +0,0 @@ -import React, { useState } from "react"; -import { Box } from "@mui/material"; -import { Outlet } from "react-router"; -import { useLocation, useNavigate } from "react-router"; -import { - AppBar, - BottomNavigation, - BottomNavigationAction, - useTheme, - useMediaQuery, - List, - ListItemText, - ListItemIcon, - ListItemButton, - ListItem, - Stack, - Avatar, -} from "@mui/material"; -import { - Home as HomeIcon, - Search as SearchIcon, - Add as AddIcon, -} from "@mui/icons-material"; -import service from "../../common/store/service"; - -type Navigation = "home" | "find-games" | "create-game" | "user"; - -const navigationPathMap: Record = { - home: "/", - "find-games": "/find-games", - "create-game": "/create-game", - user: "/user", -}; - -const TabletNavigation: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - return {children}; -}; - -const DesktopNavigation: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - return {children}; -}; - -const TabletNavigationAction: React.FC<{ - label: string; - icon: React.ReactElement; - path: string; - style?: React.CSSProperties; -}> = ({ label, icon, path, style }) => { - const navigate = useNavigate(); - const location = useLocation(); - const theme = useTheme(); - - const selectedItemIconStyle = { - color: theme.palette.common.black, - }; - - return ( - - navigate(path)} style={{ padding: 16 }}> - - {React.cloneElement(icon, { - style: - location.pathname === path - ? { ...style, ...selectedItemIconStyle } - : { ...style }, - })} - - - - ); -}; - -const DesktopNavigationAction: React.FC<{ - label: string; - icon: React.ReactElement; - path: string; - style?: React.CSSProperties; -}> = ({ label, icon, path, style }) => { - const navigate = useNavigate(); - const location = useLocation(); - const theme = useTheme(); - - const selectedItemIconStyle = { - color: theme.palette.common.black, - }; - - const selectedItemTextStyle = { - color: theme.palette.common.black, - fontWeight: "bold", - }; - - return ( - - navigate(path)} style={{ padding: 16 }}> - - {React.cloneElement(icon, { - style: - location.pathname === path - ? { ...style, ...selectedItemIconStyle } - : { ...style }, - })} - - - - - ); -}; - -const NavigationWrapper: React.FC<{ - children: React.ReactNode; -}> = ({ children }) => { - const [navigation, setNavigation] = useState("home"); - const theme = useTheme(); - const isMobile = useMediaQuery(theme.breakpoints.down("sm")); - const isTablet = useMediaQuery(theme.breakpoints.between("sm", "md")); - const isDesktop = useMediaQuery(theme.breakpoints.up("md")); - const navigate = useNavigate(); - const rootQuery = service.endpoints.getRoot.useQuery(undefined); - - return isMobile ? ( - <> - {children} - - { - setNavigation(newValue); - navigate(navigationPathMap[newValue as Navigation]); - }} - > - } - value="home" - /> - } - value="find-games" - /> - } - value="create-game" - /> - {rootQuery.isLoading ? ( - } /> - ) : ( - } - value="user" - /> - )} - - - - ) : isTablet ? ( - - - } path="/" /> - } - path="/find-games" - /> - } - path="/create-game" - /> - {rootQuery.isLoading ? ( - } - /> - ) : ( - } - style={{ width: 26, height: 26 }} - path="/user" - /> - )} - - {children} - - ) : isDesktop ? ( - - - } path="/" /> - } - path="/find-games" - /> - } - path="/create-game" - /> - {rootQuery.isLoading ? ( - } - /> - ) : ( - } - style={{ width: 26, height: 26 }} - path="/user" - /> - )} - - {children} - - ) : ( -
Not implemented
- ); -}; - -const HomeLayout: React.FC = () => { - return ( - -
- - - -
-
- ); -}; - -export { HomeLayout }; diff --git a/src/components/home-layout/index.ts b/src/components/home-layout/index.ts deleted file mode 100644 index 92d1d156..00000000 --- a/src/components/home-layout/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./home-layout" \ No newline at end of file diff --git a/src/components/index.ts b/src/components/index.ts index 89a89288..8d809fbf 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -1,4 +1,7 @@ +export * from "./channel" export * from "./drawer-navigation" -export * from "./order-list"; +export * from "./orders"; export * from "./game-card" +export * from "./map" +export * from "./panel" export * from "./query-container" \ No newline at end of file diff --git a/src/components/loading-container.tsx b/src/components/loading-container.tsx deleted file mode 100644 index e69de29b..00000000 diff --git a/src/components/order-list.tsx b/src/components/order-list.tsx deleted file mode 100644 index 892e38f9..00000000 --- a/src/components/order-list.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React from "react"; -import { useOrders } from "../common/hooks/useOrders"; -import { - Stack, - Typography, - List, - ListSubheader, - Divider, - ListItem, - ListItemText, -} from "@mui/material"; -import { formatOrderText } from "../util"; - -const OrderList: React.FC<{ - orders: NonNullable["query"]["data"]>["orders"]; -}> = ({ orders }) => { - // Reduce orders to be grouped by nation - const ordersByNation = orders.reduce((acc, order) => { - if (!acc[order.nation]) { - acc[order.nation] = []; - } - acc[order.nation].push(order); - return acc; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }, {} as Record); - return orders.length === 0 ? ( - - No order created during this turn - - ) : ( - - {Object.keys(ordersByNation).map((nation) => ( - - {nation} - - {ordersByNation[nation].map((order) => ( - - ({ - "& .MuiListItemText-secondary": { - color: - order.outcome?.outcome === "Succeeded" - ? theme.palette.success.main - : theme.palette.error.main, - }, - })} - /> - - ))} - - ))} - - ); -}; - -export { OrderList }; diff --git a/src/screens/game-detail/create-order.tsx b/src/components/orders/create-order.tsx similarity index 74% rename from src/screens/game-detail/create-order.tsx rename to src/components/orders/create-order.tsx index 04042d93..82eadaa2 100644 --- a/src/screens/game-detail/create-order.tsx +++ b/src/components/orders/create-order.tsx @@ -1,5 +1,4 @@ import { - FormLabel, Stack, Typography, ButtonGroup, @@ -9,24 +8,32 @@ import { Step, StepLabel, } from "@mui/material"; -import { QueryContainer } from "../../components"; -import { useCreateOrder } from "../../components/CreateOrder/CreateOrder.hook"; -import { CreateOrderStepIcon } from "../../components/CreateOrder/CreateOrder.step"; +import { QueryContainer } from "../"; +import { useCreateOrder } from "../CreateOrder/CreateOrder.hook"; +import { CreateOrderStepIcon } from "../CreateOrder/CreateOrder.step"; import { getOrderSummary, getNumSteps, } from "../../components/CreateOrder/CreateOrder.util"; +import { useNavigate } from "react-router"; +import { useGameDetailContext } from "../../context"; -const CreateOrder: React.FC<{ - onClose: () => void; -}> = (props) => { +const CreateOrder: React.FC = () => { + const { gameId } = useGameDetailContext(); + const navigate = useNavigate(); const { handleSelect, - handleBack, handleSubmit, isSubmitting, query: createOrderQuery, - } = useCreateOrder(props.onClose); + } = useCreateOrder(() => { + return; + }); + + const handleSubmitAndRedirect = async () => { + await handleSubmit(); + navigate(`/game/${gameId}/orders`); + }; return ( @@ -34,7 +41,6 @@ const CreateOrder: React.FC<{ const { options, order, activeStep } = data; return ( <> - Create order {order.isComplete ? ( {getOrderSummary(order)} @@ -49,8 +55,6 @@ const CreateOrder: React.FC<{ )} - {order.source && } - {!order.source && } {Array.from( @@ -64,7 +68,7 @@ const CreateOrder: React.FC<{ - - )} - - - +
{ + e.preventDefault(); + formik.handleSubmit(e); + }} + > + + {(data) => ( + + formik.setFieldValue("Desc", e.target.value)} + onBlur={formik.handleBlur} + error={formik.touched.Desc && Boolean(formik.errors.Desc)} + helperText={formik.touched.Desc && formik.errors.Desc} + disabled={isSubmitting} + /> + formik.setFieldValue("Variant", e.target.value)} + onBlur={formik.handleBlur} + error={formik.touched.Variant && Boolean(formik.errors.Variant)} + helperText={formik.touched.Variant && formik.errors.Variant} + disabled={isSubmitting} + > + {data.map((variant) => ( + + {variant.Name} + + ))} + + + } + label="Private" + /> + + + )} + +
); }; diff --git a/src/screens/home/find-games.tsx b/src/screens/home/find-games.tsx index 99389374..daf71d1a 100644 --- a/src/screens/home/find-games.tsx +++ b/src/screens/home/find-games.tsx @@ -1,7 +1,6 @@ import React from "react"; import { List } from "@mui/material"; import { QueryContainer } from "../../components"; -import { ScreenTopBar } from "./screen-top-bar"; import { service } from "../../common"; import { GameCard } from "../../components"; @@ -20,16 +19,11 @@ const FindGames: React.FC = () => { const { query } = useFindGames(); return ( - <> - - - - {(games) => - games.map((game) => ) - } - - - + + + {(games) => games.map((game) => )} + + ); }; diff --git a/src/screens/home/game-info.tsx b/src/screens/home/game-info.tsx index 7856d37b..fc42ad7c 100644 --- a/src/screens/home/game-info.tsx +++ b/src/screens/home/game-info.tsx @@ -23,7 +23,6 @@ import { } from "@mui/icons-material"; import { QueryContainer } from "../../components"; import { mergeQueries, service, useGetVariantQuery } from "../../common"; -import { ScreenTopBar } from "./screen-top-bar"; import { useParams } from "react-router"; const styles: Styles = { @@ -98,7 +97,6 @@ const GameInfo: React.FC = () => { return ( <> - {(data) => ( <> diff --git a/src/screens/home/player-info.tsx b/src/screens/home/player-info.tsx index e75a93cb..b7e30832 100644 --- a/src/screens/home/player-info.tsx +++ b/src/screens/home/player-info.tsx @@ -8,7 +8,6 @@ import { } from "@mui/material"; import { QueryContainer } from "../../components"; import { service } from "../../common"; -import { ScreenTopBar } from "./screen-top-bar"; import { useParams } from "react-router"; const usePlayerInfo = () => { @@ -24,23 +23,20 @@ const PlayerInfo: React.FC = () => { const { query } = usePlayerInfo(); return ( - <> - - - {(data) => ( - - {data.Members.map((member) => ( - - - - - - - ))} - - )} - - + + {(data) => ( + + {data.Members.map((member) => ( + + + + + + + ))} + + )} + ); }; diff --git a/src/screens/home/profile.tsx b/src/screens/home/profile.tsx index ab38a26c..afcd6303 100644 --- a/src/screens/home/profile.tsx +++ b/src/screens/home/profile.tsx @@ -11,7 +11,6 @@ import { MoreHoriz } from "@mui/icons-material"; import { QueryContainer } from "../../components"; import { actions, AppDispatch, service } from "../../common"; import { useDispatch } from "react-redux"; -import { ScreenTopBar } from "./screen-top-bar"; const styles: Styles = { root: { @@ -52,31 +51,26 @@ const Profile: React.FC = () => { }; return ( - <> - - - {(data) => ( - - - - - - {data.Name} - - - - - - - - Logout - - - + + {(data) => ( + + + - )} - - + + {data.Name} + + + + + + + Logout + + + + )} + ); }; diff --git a/src/screens/home/screen-top-bar.tsx b/src/screens/home/screen-top-bar.tsx deleted file mode 100644 index 39fe3552..00000000 --- a/src/screens/home/screen-top-bar.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { IconButton, Stack, Typography } from "@mui/material"; -import { KeyboardBackspace as BackIcon } from "@mui/icons-material"; -import { useNavigate } from "react-router"; - -type ScreenTopBarProps = { - title: string; - menu?: React.ReactNode; -}; - -const styles: Styles = { - root: (theme) => ({ - borderBottom: `1px solid ${theme.palette.divider}`, - }), - iconButton: (theme) => ({ - padding: 0, - color: theme.palette.text.primary, - }), -}; - -const ScreenTopBar: React.FC = (props) => { - const navigate = useNavigate(); - - return ( - - - navigate(-1)} sx={styles.iconButton}> - - - {props.title} - - {props.menu} - - ); -}; - -export { ScreenTopBar }; diff --git a/src/screens/index.ts b/src/screens/index.ts index 8ea5bcba..a6290aee 100644 --- a/src/screens/index.ts +++ b/src/screens/index.ts @@ -1,3 +1,2 @@ -export * from "./game-detail" export * from "./logged-out" export * from "./home" \ No newline at end of file diff --git a/src/screens/game-detail/game-detail-layout.tsx b/src/screens/mobile/game-detail-layout.tsx similarity index 93% rename from src/screens/game-detail/game-detail-layout.tsx rename to src/screens/mobile/game-detail-layout.tsx index 1282c2ca..b191e59e 100644 --- a/src/screens/game-detail/game-detail-layout.tsx +++ b/src/screens/mobile/game-detail-layout.tsx @@ -1,9 +1,8 @@ -import React from "react"; -import { Outlet } from "react-router"; import { GameDetailContextProvider, SelectedPhaseContextProvider, } from "../../context"; +import { Outlet } from "react-router"; const GameDetailLayout: React.FC = () => { return ( diff --git a/src/screens/mobile/game-detail-primary-screen-layout.tsx b/src/screens/mobile/game-detail-primary-screen-layout.tsx new file mode 100644 index 00000000..07ec9f65 --- /dev/null +++ b/src/screens/mobile/game-detail-primary-screen-layout.tsx @@ -0,0 +1,130 @@ +import { + Stack, + BottomNavigation, + BottomNavigationAction, + AppBar, + IconButton, + Typography, + Divider, +} from "@mui/material"; +import { + Map as MapIcon, + Chat as ChatIcon, + Gavel as OrdersIcon, + KeyboardBackspace as BackIcon, +} from "@mui/icons-material"; +import { Outlet, useLocation, useNavigate } from "react-router"; +import { useGameDetailContext } from "../../context"; +import React, { useEffect, useState } from "react"; +import { GameDetailMenu } from "../game-detail-menu"; + +const styles: Styles = { + root: { + flexGrow: 1, + height: "100vh", + overflow: "hidden", + }, + appBar: { + padding: 1, + alignItems: "center", + position: "relative", + display: "flex", + flexDirection: "row", + justifyContent: "space-between", + "& h1": { + margin: 0, + }, + gap: 1, + }, + backButtonTitleContainer: { + display: "flex", + flexDirection: "row", + alignItems: "center", + width: "100%", + gap: 1, + }, + screen: { + height: "calc(100vh - 120px)", + }, +}; + +const NavigationItems = [ + { + label: "Map", + icon: , + value: (gameId: string) => `/game/${gameId}`, + }, + { + label: "Orders", + icon: , + value: (gameId: string) => `/game/${gameId}/orders`, + }, + { + label: "Chat", + icon: , + value: (gameId: string) => `/game/${gameId}/chat`, + }, +] as const; + +type GameDetailPrimaryScreenLayoutProps = { + title: string | React.ReactNode; +}; + +const GameDetailPrimaryScreenLayout: React.FC< + GameDetailPrimaryScreenLayoutProps +> = (props) => { + const { gameId } = useGameDetailContext(); + const navigate = useNavigate(); + const location = useLocation(); + const [navigation, setNavigation] = useState(location.pathname); + + const handleNavigateBack = () => { + navigate(`/`); + }; + + const handleNavigationChange = (newValue: string) => { + setNavigation(newValue); + navigate(newValue); + }; + + useEffect(() => { + setNavigation(location.pathname); + }, [location.pathname]); + + return ( + + + + + + + {typeof props.title === "string" ? ( + {props.title} + ) : ( + props.title + )} + + + + + + + + handleNavigationChange(newValue)} + > + {NavigationItems.map((item) => ( + + ))} + + + ); +}; + +export { GameDetailPrimaryScreenLayout }; diff --git a/src/screens/mobile/game-detail-secondary-screen-layout.tsx b/src/screens/mobile/game-detail-secondary-screen-layout.tsx new file mode 100644 index 00000000..8a08b52b --- /dev/null +++ b/src/screens/mobile/game-detail-secondary-screen-layout.tsx @@ -0,0 +1,15 @@ +import { NavigateFunction } from "react-router"; +import * as Desktop from "../desktop"; + +type GameDetailSecondaryScreenLayoutProps = { + title: string | React.ReactNode; + onNavigateBack: (navigate: NavigateFunction, gameId: string) => void; +}; + +const GameDetailSecondaryScreenLayout: React.FC< + GameDetailSecondaryScreenLayoutProps +> = (props) => { + return ; +}; + +export { GameDetailSecondaryScreenLayout }; diff --git a/src/screens/mobile/index.ts b/src/screens/mobile/index.ts new file mode 100644 index 00000000..9f7a0428 --- /dev/null +++ b/src/screens/mobile/index.ts @@ -0,0 +1,3 @@ +export * from "./game-detail-layout"; +export * from "./game-detail-primary-screen-layout"; +export * from "./game-detail-secondary-screen-layout"; \ No newline at end of file diff --git a/src/screens/parse-map.tsx b/src/screens/parse-map.tsx new file mode 100644 index 00000000..17a6bec7 --- /dev/null +++ b/src/screens/parse-map.tsx @@ -0,0 +1,72 @@ +import { useState } from "react"; +import { Box, TextField, Typography, Button } from "@mui/material"; +import { parseSvg } from "../common/map/map.parse"; + +const process = (input: string): string => { + // Implement your processing logic here + const parsed = parseSvg(input); + console.log(parsed); + return JSON.stringify(parsed, null, 2); +}; + +const ParseMap = () => { + const [input, setInput] = useState(""); + const [processedContent, setProcessedContent] = useState(""); + + const handleProcess = () => { + setProcessedContent(process(input)); + }; + + const handleCopyToClipboard = () => { + if (processedContent) { + navigator.clipboard + .writeText(processedContent) + .then(() => { + console.log("Copied to clipboard"); + }) + .catch((err) => { + console.error("Failed to copy: ", err); + }); + } + }; + + return ( + + setInput(e.target.value)} + fullWidth + /> + + + {processedContent && ( + <> + + Processed Content: + + + {processedContent} + + + )} + + ); +}; +export { ParseMap }; diff --git a/src/theme.ts b/src/theme.ts index 53f98ce3..465e800c 100644 --- a/src/theme.ts +++ b/src/theme.ts @@ -66,6 +66,15 @@ const theme = createTheme({ textAlign: "left", }, }, + }, + MuiMenuItem: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiListItemIcon-root": { + color: theme.palette.text.primary, + }, + }), + }, } } }); From 80e0df4d3a33cf87cfd4826a2d062786fb260334 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:02:00 +0000 Subject: [PATCH 44/64] s --- src/components/Map/{Map.tsx => temp.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/Map/{Map.tsx => temp.tsx} (100%) diff --git a/src/components/Map/Map.tsx b/src/components/Map/temp.tsx similarity index 100% rename from src/components/Map/Map.tsx rename to src/components/Map/temp.tsx From 9abe0ff1c9b8e395b2f51e1707885ddd9d89b590 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:02:22 +0000 Subject: [PATCH 45/64] f --- src/components/{Map => map-temp}/index.ts | 0 src/components/{Map => map-temp}/temp.tsx | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename src/components/{Map => map-temp}/index.ts (100%) rename src/components/{Map => map-temp}/temp.tsx (100%) diff --git a/src/components/Map/index.ts b/src/components/map-temp/index.ts similarity index 100% rename from src/components/Map/index.ts rename to src/components/map-temp/index.ts diff --git a/src/components/Map/temp.tsx b/src/components/map-temp/temp.tsx similarity index 100% rename from src/components/Map/temp.tsx rename to src/components/map-temp/temp.tsx From bef91f316328be7ed8551cdeb775a58b8af8b6ae Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:02:40 +0000 Subject: [PATCH 46/64] s --- src/components/map/index.ts | 1 + src/components/map/temp.tsx | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 src/components/map/index.ts create mode 100644 src/components/map/temp.tsx diff --git a/src/components/map/index.ts b/src/components/map/index.ts new file mode 100644 index 00000000..fc155f27 --- /dev/null +++ b/src/components/map/index.ts @@ -0,0 +1 @@ +export * from "./map"; \ No newline at end of file diff --git a/src/components/map/temp.tsx b/src/components/map/temp.tsx new file mode 100644 index 00000000..9df9a440 --- /dev/null +++ b/src/components/map/temp.tsx @@ -0,0 +1,21 @@ +import { useMap } from "../../common"; +import { QueryContainer } from "../query-container"; + +const Map: React.FC = () => { + const { query } = useMap(); + return ( + + {(data) => ( +
+ )} + + ); +}; + +export { Map }; From f7cd50a42e566e779cb62cb5eaa2d8c88405d2ac Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:03:42 +0000 Subject: [PATCH 47/64] t --- src/components/map/index.ts | 1 - .../{map-temp/temp.tsx => map/map.tsx} | 0 .../{map-temp/index.ts => map/temp} | 0 src/components/map/temp.tsx | 21 ------------------- 4 files changed, 22 deletions(-) delete mode 100644 src/components/map/index.ts rename src/components/{map-temp/temp.tsx => map/map.tsx} (100%) rename src/components/{map-temp/index.ts => map/temp} (100%) delete mode 100644 src/components/map/temp.tsx diff --git a/src/components/map/index.ts b/src/components/map/index.ts deleted file mode 100644 index fc155f27..00000000 --- a/src/components/map/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./map"; \ No newline at end of file diff --git a/src/components/map-temp/temp.tsx b/src/components/map/map.tsx similarity index 100% rename from src/components/map-temp/temp.tsx rename to src/components/map/map.tsx diff --git a/src/components/map-temp/index.ts b/src/components/map/temp similarity index 100% rename from src/components/map-temp/index.ts rename to src/components/map/temp diff --git a/src/components/map/temp.tsx b/src/components/map/temp.tsx deleted file mode 100644 index 9df9a440..00000000 --- a/src/components/map/temp.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import { useMap } from "../../common"; -import { QueryContainer } from "../query-container"; - -const Map: React.FC = () => { - const { query } = useMap(); - return ( - - {(data) => ( -
- )} - - ); -}; - -export { Map }; From c19ada07a4f09f482b1c8686a35ff27944b18ac9 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:03:55 +0000 Subject: [PATCH 48/64] t --- src/components/map/{temp => index.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/map/{temp => index.ts} (100%) diff --git a/src/components/map/temp b/src/components/map/index.ts similarity index 100% rename from src/components/map/temp rename to src/components/map/index.ts From d4e4447b0b33e122676c618556a1f1732a9a6fee Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:11:00 +0000 Subject: [PATCH 49/64] Add web.config for URL rewriting to support React routes --- web.config | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 web.config diff --git a/web.config b/web.config new file mode 100644 index 00000000..f3eed865 --- /dev/null +++ b/web.config @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file From affce4f2d03d9ed72db2a0d62715c54632159131 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Fri, 7 Feb 2025 18:25:39 +0000 Subject: [PATCH 50/64] Replace web.config with staticwebapp.config.json for React route handling --- staticwebapp.config.json | 10 ++++++++++ web.config | 16 ---------------- 2 files changed, 10 insertions(+), 16 deletions(-) create mode 100644 staticwebapp.config.json delete mode 100644 web.config diff --git a/staticwebapp.config.json b/staticwebapp.config.json new file mode 100644 index 00000000..11d03aaf --- /dev/null +++ b/staticwebapp.config.json @@ -0,0 +1,10 @@ +{ + "navigationFallback": { + "rewrite": "index.html", + "exclude": [ + "*.{svg,png,jpg,gif}", + "*.{css,scss}", + "*.js" + ] + } +} \ No newline at end of file diff --git a/web.config b/web.config deleted file mode 100644 index f3eed865..00000000 --- a/web.config +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - - - - - - - - \ No newline at end of file From 33831f81bbc8d2bc0b9a818cf2aa92bb67d2f56d Mon Sep 17 00:00:00 2001 From: John McDowell Date: Sun, 9 Feb 2025 12:49:43 +0000 Subject: [PATCH 51/64] Add spacing component to HomeLayout for mobile view --- src/screens/home/layout.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/screens/home/layout.tsx b/src/screens/home/layout.tsx index 3154be50..3905e046 100644 --- a/src/screens/home/layout.tsx +++ b/src/screens/home/layout.tsx @@ -120,6 +120,7 @@ const HomeLayout: React.FC = () => { {isMobile ? ( <> + Date: Sun, 9 Feb 2025 13:03:15 +0000 Subject: [PATCH 52/64] Refactor GameCard component layout for improved readability and structure --- src/components/game-card.tsx | 71 ++++++++++++++++++------------------ 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/src/components/game-card.tsx b/src/components/game-card.tsx index c5633f85..f09c9f34 100644 --- a/src/components/game-card.tsx +++ b/src/components/game-card.tsx @@ -193,48 +193,47 @@ const GameCard: React.FC<{ {game.Variant} - - {game.Desc} - - } - secondary={ - - - {game.Variant} - - {game.PhaseLengthMinutes} - - - + + + {game.Desc} + + } + /> + + + {game.Variant} + {game.PhaseLengthMinutes} - } - /> + + + ); }; From 58a508c31126950fe7f5f695a0f2bdba894ff218 Mon Sep 17 00:00:00 2001 From: John McDowell Date: Sun, 9 Feb 2025 13:03:22 +0000 Subject: [PATCH 53/64] Increase token duration in login URL to 100 years for extended session management --- src/screens/logged-out/login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/screens/logged-out/login.tsx b/src/screens/logged-out/login.tsx index aa200119..7e86c820 100644 --- a/src/screens/logged-out/login.tsx +++ b/src/screens/logged-out/login.tsx @@ -3,7 +3,7 @@ import { Button, Typography, Box, Stack } from "@mui/material"; const getLoginUrl = (): string => { const redirectUrl = location.href; - const tokenDuration = 60 * 60 * 24; + const tokenDuration = 60 * 60 * 24 * 365 * 100; return `https://diplicity-engine.appspot.com/Auth/Login?redirect-to=${encodeURI( redirectUrl )}&token-duration=${tokenDuration}`; From 4caa514ed69a419cc778a130c2a4b66c63a4d8ac Mon Sep 17 00:00:00 2001 From: John McDowell Date: Sun, 9 Feb 2025 18:15:38 +0000 Subject: [PATCH 54/64] Add CreateChannelAction component and update channel-related exports --- src/Router.tsx | 62 ++++++ src/common/schema/list-messages.ts | 2 +- src/components/channel/channel-list.tsx | 14 +- src/components/channel/channel.tsx | 10 +- .../channel/create-channel-action.tsx | 30 +++ src/components/channel/create-channel.tsx | 191 +++++++++++++++++- src/components/channel/index.ts | 3 +- 7 files changed, 292 insertions(+), 20 deletions(-) create mode 100644 src/components/channel/create-channel-action.tsx diff --git a/src/Router.tsx b/src/Router.tsx index ca7927c4..e2d2e09a 100644 --- a/src/Router.tsx +++ b/src/Router.tsx @@ -25,6 +25,10 @@ import { ChannelTextField, ChannelList, Map, + CreateChannel, + CreateChannelAction, + CreateChannelTetxField, + CreateChannelContextProvider, } from "./components"; import { ChannelContextProvider } from "./context/channel-context"; import { PhaseSelect } from "./components/phase-select"; @@ -147,6 +151,9 @@ const Router: React.FC = () => { + + + } /> @@ -214,6 +221,32 @@ const Router: React.FC = () => { } /> + + navigate(`/game/${gameId}/chat`) + } + /> + } + > + + + + + + + + + + + } + /> + @@ -279,6 +312,9 @@ const Router: React.FC = () => { + + + } /> @@ -346,6 +382,32 @@ const Router: React.FC = () => { } /> + + navigate(`/game/${gameId}/chat`) + } + /> + } + > + + + + + + + + + + + } + /> + diff --git a/src/common/schema/list-messages.ts b/src/common/schema/list-messages.ts index 2d0548cb..9cee5432 100644 --- a/src/common/schema/list-messages.ts +++ b/src/common/schema/list-messages.ts @@ -5,7 +5,7 @@ const messageSchema = z.object({ ID: z.string(), Sender: z.string(), Body: z.string(), - CreatedAt: z.string().transform((value) => new Date(value)), + CreatedAt: z.string() }); const listMessagesSchema = listApiResponseSchema(apiResponseSchema(messageSchema)); diff --git a/src/components/channel/channel-list.tsx b/src/components/channel/channel-list.tsx index f5926649..b2524b1b 100644 --- a/src/components/channel/channel-list.tsx +++ b/src/components/channel/channel-list.tsx @@ -1,12 +1,5 @@ import React from "react"; -import { - Avatar, - List, - ListItem, - ListItemAvatar, - ListItemButton, - ListItemText, -} from "@mui/material"; +import { List, ListItem, ListItemButton, ListItemText } from "@mui/material"; import { useLocation, useNavigate } from "react-router"; import { useGameDetailContext } from "../../context"; import { service, useGetVariantQuery, mergeQueries } from "../../common"; @@ -65,8 +58,6 @@ const ChannelList: React.FC = () => { navigate(`/game/${gameId}/chat/channel/${name}`); }; - // Use regex to get the channel name from the URL - // e.g. .../channel/Germany,Italy -> Germany,Italy const selectedChannel = location.pathname.match(/\/channel\/(.*)/)?.[1]; return ( @@ -83,9 +74,6 @@ const ChannelList: React.FC = () => { } > handleChannelClick(channel.name)}> - - {channel.name.charAt(0)} - { acc[date].push({ body: message.Body, sender: message.Sender, - date: message.CreatedAt, + date: new Date(message.CreatedAt), + flag: + message.Sender === "Diplicity" + ? "/otto.png" + : variant.Flags[message.Sender], }); return acc; - }, {} as Record); + }, {} as Record); return { messages: groupedMessages, @@ -156,7 +160,7 @@ const Channel: React.FC = () => { {data.messages[date].map((message, index) => ( - {message.sender[0]} + {message.sender[0]} { + const { gameId } = useGameDetailContext(); + const navigate = useNavigate(); + + const handleCreateChannel = () => { + navigate(`/game/${gameId}/chat/channel/create`); + }; + + return ( + + + + + + ); +}; + +export { CreateChannelAction }; diff --git a/src/components/channel/create-channel.tsx b/src/components/channel/create-channel.tsx index 88681d7e..084b3b80 100644 --- a/src/components/channel/create-channel.tsx +++ b/src/components/channel/create-channel.tsx @@ -1,5 +1,192 @@ +import { + Stack, + List, + ListItem, + Checkbox, + ListItemButton, + ListItemAvatar, + Avatar, + ListItemText, + TextField, + IconButton, +} from "@mui/material"; +import { Send as SendIcon } from "@mui/icons-material"; +import React from "react"; +import { useNavigate } from "react-router"; +import { service, mergeQueries, useGetVariantQuery } from "../../common"; +import { useGameDetailContext } from "../../context"; +import { QueryContainer } from "../query-container"; + +type CreateChannelContextType = { + selectedMembers: string[]; + setSelectedMembers: React.Dispatch>; +}; + +const CreateChannelContext = React.createContext< + CreateChannelContextType | undefined +>(undefined); + +const CreateChannelContextProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const [selectedMembers, setSelectedMembers] = React.useState([]); + + return ( + + {children} + + ); +}; + +const useCreateChannel = () => { + const { gameId } = useGameDetailContext(); + const { selectedMembers, setSelectedMembers } = React.useContext( + CreateChannelContext + ) as CreateChannelContextType; + const [message, setMessage] = React.useState(""); + const getRootQuery = service.endpoints.getRoot.useQuery(undefined); + const getGameQuery = service.endpoints.getGame.useQuery(gameId); + const getVariantQuery = useGetVariantQuery(gameId); + const [createMessage, createMessageMutation] = + service.endpoints.createMessage.useMutation(); + + const query = mergeQueries( + [getVariantQuery, getRootQuery, getGameQuery], + (variant, user, game) => { + return { + userNation: game.Members.find((member) => member.User.Id === user.Id) + ?.Nation, + members: game.Members.filter( + (member) => member.User.Id !== user.Id + ).map((member) => { + return { + ...member, + flag: variant.Flags[member.Nation], + }; + }), + }; + } + ); + + const handleSubmit = async () => { + if (!query.data) throw new Error("Data is not available yet"); + if (!query.data.userNation) throw new Error("User nation is not available"); + return createMessage({ + gameId: gameId, + ChannelMembers: [...selectedMembers, query.data.userNation], + Body: message, + }); + }; + + const handleToggle = (memberId: string) => { + setSelectedMembers((prevSelected) => + prevSelected.includes(memberId) + ? prevSelected.filter((id) => id !== memberId) + : [...prevSelected, memberId] + ); + }; + + const isSubmitting = createMessageMutation.isLoading; + + return { + query, + handleSubmit, + message, + setMessage, + selectedMembers, + handleToggle, + isSubmitting, + }; +}; + const CreateChannel: React.FC = () => { - return
Hello world
; + const { query, selectedMembers, handleToggle, isSubmitting } = + useCreateChannel(); + + return ( + + {(data) => ( + + + + {data.members.map((member) => ( + handleToggle(member.Nation)} + checked={selectedMembers.includes(member.Nation)} + disableRipple + disabled={isSubmitting} + /> + } + > + handleToggle(member.Nation)} + > + + {member.Nation[0]} + + + + + ))} + + + + )} + + ); +}; + +const CreateChannelTetxField: React.FC = () => { + const { gameId } = useGameDetailContext(); + const navigate = useNavigate(); + + const { message, setMessage, handleSubmit, isSubmitting, selectedMembers } = + useCreateChannel(); + + const handleSubmitAndRedirect = async () => { + const response = await handleSubmit(); + if (response.data) { + const channelName = response.data.ChannelMembers.join(","); + navigate(`/game/${gameId}/chat/channel/${channelName}`); + } + }; + + return ( + + setMessage(e.target.value)} + fullWidth + disabled={isSubmitting} + /> + + + + + ); }; -export { CreateChannel }; +export { CreateChannel, CreateChannelTetxField, CreateChannelContextProvider }; diff --git a/src/components/channel/index.ts b/src/components/channel/index.ts index 1d91067d..5548552e 100644 --- a/src/components/channel/index.ts +++ b/src/components/channel/index.ts @@ -1,3 +1,4 @@ export * from "./channel"; export * from "./channel-list"; -export * from "./create-channel"; \ No newline at end of file +export * from "./create-channel"; +export * from "./create-channel-action"; \ No newline at end of file From 78f380d69c035bec3a2f148d1cc77912577ea69d Mon Sep 17 00:00:00 2001 From: John McDowell Date: Wed, 12 Feb 2025 09:00:51 +0000 Subject: [PATCH 55/64] Add interactive map --- .vscode/tasks.json | 20 + .../InteractiveMap/InteractiveMap.stories.tsx | 15 - .../InteractiveMap/InteractiveMap.tsx | 143 ------- .../interactive-map.stories.tsx | 56 +++ .../interactive-map/interactive-map.tsx | 376 ++++++++++++++++++ .../interactive-map/shapes/arrow.stories.tsx | 60 +++ .../interactive-map/shapes/arrow.tsx | 87 ++++ .../interactive-map/shapes/cross.tsx | 74 ++++ .../shapes/curved-arrow.stories.tsx | 77 ++++ .../interactive-map/shapes/curved-arrow.tsx | 121 ++++++ .../interactive-map/shapes/order-arrow.tsx | 127 ++++++ .../shapes/parallel-curves.tsx | 70 ++++ 12 files changed, 1068 insertions(+), 158 deletions(-) delete mode 100644 src/components/InteractiveMap/InteractiveMap.stories.tsx delete mode 100644 src/components/InteractiveMap/InteractiveMap.tsx create mode 100644 src/components/interactive-map/interactive-map.stories.tsx create mode 100644 src/components/interactive-map/interactive-map.tsx create mode 100644 src/components/interactive-map/shapes/arrow.stories.tsx create mode 100644 src/components/interactive-map/shapes/arrow.tsx create mode 100644 src/components/interactive-map/shapes/cross.tsx create mode 100644 src/components/interactive-map/shapes/curved-arrow.stories.tsx create mode 100644 src/components/interactive-map/shapes/curved-arrow.tsx create mode 100644 src/components/interactive-map/shapes/order-arrow.tsx create mode 100644 src/components/interactive-map/shapes/parallel-curves.tsx diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3f9d469d..ff6f1f9b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -21,6 +21,26 @@ "runOn": "folderOpen" } }, + { + "label": "storybook", + "type": "shell", + "command": "npm run storybook", + "presentation": { + "echo": false, + "reveal": "always", + "focus": false, + "panel": "shared", + "showReuseMessage": false, + "clear": true, + "close": false + }, + "options": { + "cwd": "${workspaceFolder}" + }, + "runOptions": { + "runOn": "folderOpen" + } + }, { "label": "build:watch", "type": "shell", diff --git a/src/components/InteractiveMap/InteractiveMap.stories.tsx b/src/components/InteractiveMap/InteractiveMap.stories.tsx deleted file mode 100644 index 2f6ce2ad..00000000 --- a/src/components/InteractiveMap/InteractiveMap.stories.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { Meta, StoryObj } from "@storybook/react"; -import { InteractiveMap } from "./InteractiveMap"; -import classical from "../../data/map/classical.json"; - -export default { - title: "Components/InteractiveMap", - component: InteractiveMap, - args: { - map: classical, - }, -} as Meta; - -type Story = StoryObj; - -export const Default: Story = {}; diff --git a/src/components/InteractiveMap/InteractiveMap.tsx b/src/components/InteractiveMap/InteractiveMap.tsx deleted file mode 100644 index a14214ef..00000000 --- a/src/components/InteractiveMap/InteractiveMap.tsx +++ /dev/null @@ -1,143 +0,0 @@ -import React, { useState } from "react"; -import { Map } from "../../common/map/map.parse.types"; - -type InteractiveMapProps = { - map: Map; -}; - -const HOVER_STROKE_WIDTH = 1; -const HOVER_STROKE_COLOR = "white"; -const HOVER_FILL = "rgba(255, 255, 255, 0.6)"; - -const SELECTED_STROKE_WIDTH = 3; -const SELECTED_STROKE_COLOR = "white"; -const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; - -const DEFAULT_FILL = "transparent"; - -const InteractiveMap: React.FC = ({ map }) => { - const [hoveredProvince, setHoveredProvince] = useState(null); - const [selectedProvince, setSelectedProvince] = useState(null); - - const getFill = (provinceId: string) => { - if (selectedProvince === provinceId) return SELECTED_FILL; - if (hoveredProvince === provinceId) return HOVER_FILL; - return DEFAULT_FILL; - }; - - const getStroke = (provinceId: string) => { - if (selectedProvince === provinceId) return SELECTED_STROKE_COLOR; - if (hoveredProvince === provinceId) return HOVER_STROKE_COLOR; - return "none"; - }; - - const getStrokeWidth = (provinceId: string) => { - if (selectedProvince === provinceId) return SELECTED_STROKE_WIDTH; - if (hoveredProvince === provinceId) return HOVER_STROKE_WIDTH; - return 1; - }; - - return ( - - - - - - - {map.backgroundElements.map((element, index) => ( - - - - ))} - {map.provinces.map((province) => ( - - setHoveredProvince(province.id)} - onMouseLeave={() => setHoveredProvince(null)} - onClick={() => setSelectedProvince(province.id)} - /> - {province.supplyCenter && ( - - - - - )} - - ))} - {map.provinces.map( - (province) => - province.text && ( - - {province.text.value} - - ) - )} - {map.borders.map((element, index) => ( - - ))} - {map.impassableProvinces.map((element, index) => ( - - ))} - - ); -}; - -export { InteractiveMap }; diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx new file mode 100644 index 00000000..a50cc724 --- /dev/null +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -0,0 +1,56 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { InteractiveMap } from "./interactive-map"; +import classical from "../../data/map/classical.json"; + +export default { + title: "Components/InteractiveMap", + component: InteractiveMap, + args: { + map: classical, + units: { + lon: { + unitType: "army", + nation: "England", + }, + lvp: { + unitType: "fleet", + nation: "England", + }, + edi: { + unitType: "army", + nation: "England", + }, + }, + nationColors: { + England: "rgb(255, 0, 0)", + France: "rgb(0, 0, 255)", + Germany: "rgb(0, 255, 0)", + Italy: "rgb(255, 255, 0)", + Austria: "rgb(255, 0, 255)", + Russia: "rgb(0, 255, 255)", + }, + supplyCenters: { + lon: "England", + lvp: "England", + edi: "England", + }, + orders: { + lvp: { + type: "move", + target: "wal", + aux: "edi", + outcome: "succeeded", + }, + lon: { + type: "support", + target: "wal", + aux: "lvp", + outcome: "succeeded", + }, + }, + }, +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx new file mode 100644 index 00000000..aff05886 --- /dev/null +++ b/src/components/interactive-map/interactive-map.tsx @@ -0,0 +1,376 @@ +import React, { useState } from "react"; +import { Map } from "../../common/map/map.parse.types"; +import { MoveArrow, SupportArrow } from "./shapes/order-arrow"; +import { Arrow } from "./shapes/arrow"; +import { Cross } from "./shapes/cross"; +import { CurvedArrow } from "./shapes/curved-arrow"; +import { ParallelCurves } from "./shapes/parallel-curves"; + +type InteractiveMapProps = { + map: Map; + units: { + [provinceId: string]: { unitType: "army" | "fleet"; nation: string }; + }; + supplyCenters: { + [provinceId: string]: string; + }; + nationColors: { [nation: string]: string }; + orders: { + [provinceId: string]: { + source: string; + type: "move" | "support" | "convoy" | "hold"; + target?: string; + aux?: string; + outcome?: "succeeded" | "failed"; + }; + }; +}; + +const HOVER_STROKE_WIDTH = 3; +const HOVER_STROKE_COLOR = "white"; +const HOVER_FILL = "rgba(255, 255, 255, 0.6)"; + +const SELECTED_STROKE_WIDTH = 3; +const SELECTED_STROKE_COLOR = "white"; +const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; + +const DEFAULT_FILL = "transparent"; + +const UNIT_RADIUS = 10; + +const ORDER_STROKE_WIDTH = 1; +const ORDER_LINE_WIDTH = 5; +const ORDER_ARROW_WIDTH = 7.5; +const ORDER_ARROW_LENGTH = 10; +const ORDER_DASH_LENGTH = 5; +const ORDER_DASH_SPACING = 2.5; + +type InteractiveMapContextType = { + getProvince: (provinceId: string) => { + fill: string; + center: { x: number; y: number }; + unitCenter: { x: number; y: number }; + }; +}; + +const InteractiveMapContext = React.createContext< + InteractiveMapContextType | undefined +>(undefined); + +const InteractiveMapContextProvider: React.FC< + React.PropsWithChildren +> = (props) => { + const [selectedProvince, setSelectedProvince] = useState(null); + const [hoveredProvince, setHoveredProvince] = useState(null); + + const getFill = (provinceId: string) => { + const isSelected = selectedProvince === provinceId; + const isHovered = hoveredProvince === provinceId; + if (props.supplyCenters[provinceId]) { + const color = props.nationColors[props.supplyCenters[provinceId]]; + return color.replace( + /rgb(a?)\((\d+), (\d+), (\d+)(, [\d.]+)?\)/, + // "rgba($2, $3, $4, 0.4)" + `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})` + ); + } + if (selectedProvince === provinceId) return SELECTED_FILL; + if (hoveredProvince === provinceId) return HOVER_FILL; + return DEFAULT_FILL; + }; + + const getProvince = (provinceId: string) => { + const province = props.map.provinces.find((p) => p.id === provinceId); + if (!province) throw new Error(`Province ${provinceId} not found`); + return { + fill: getFill(provinceId), + center: province.center, + unitCenter: { x: province.center.x - 10, y: province.center.y - 10 }, + }; + }; + + return ( + + {props.children} + + ); +}; + +const useProvince = (provinceId: string) => { + const context = React.useContext(InteractiveMapContext); + if (!context) + throw new Error("Must be used in InteractiveMapContextProvider"); + return context.getProvince(provinceId); +}; + +const Order: React.FC<{ + type: "move" | "support" | "convoy" | "hold"; + source: string; + target: string; + aux: string; +}> = (props) => { + const source = useProvince(props.source); + const target = useProvince(props.target); + const aux = useProvince(props.aux); + + // TODO render moves after supports + if (props.type === "move") { + return ( + ( + + )} + /> + ); + } + if (props.type === "support") { + return ( + ( + // + // )} + /> + ); + } +}; + +const InteractiveMap: React.FC = (props) => { + const [hoveredProvince, setHoveredProvince] = useState(null); + const [selectedProvince, setSelectedProvince] = useState(null); + + const getFill = (provinceId: string) => { + const isSelected = selectedProvince === provinceId; + const isHovered = hoveredProvince === provinceId; + if (props.supplyCenters[provinceId]) { + const color = props.nationColors[props.supplyCenters[provinceId]]; + return color.replace( + /rgb(a?)\((\d+), (\d+), (\d+)(, [\d.]+)?\)/, + // "rgba($2, $3, $4, 0.4)" + `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})` + ); + } + if (selectedProvince === provinceId) return SELECTED_FILL; + if (hoveredProvince === provinceId) return HOVER_FILL; + return DEFAULT_FILL; + }; + + const getStroke = (provinceId: string) => { + if (selectedProvince === provinceId) return SELECTED_STROKE_COLOR; + if (hoveredProvince === provinceId) return HOVER_STROKE_COLOR; + return "none"; + }; + + const getStrokeWidth = (provinceId: string) => { + if (selectedProvince === provinceId) return SELECTED_STROKE_WIDTH; + if (hoveredProvince === provinceId) return HOVER_STROKE_WIDTH; + return 1; + }; + + return ( + + + + + + + + + + + {props.map.backgroundElements.map((element, index) => ( + + + + ))} + {props.map.provinces.map((province) => { + return ( + + setHoveredProvince(province.id)} + onMouseLeave={() => setHoveredProvince(null)} + onClick={() => setSelectedProvince(province.id)} + /> + {province.supplyCenter && ( + + + + + )} + + ); + })} + {props.map.provinces.map( + (province) => + province.text && ( + + {province.text.value} + + ) + )} + {props.map.borders.map((element, index) => ( + + ))} + {props.map.impassableProvinces.map((element, index) => ( + + ))} + {Object.entries(props.units).map(([provinceId, unit]) => { + const province = props.map.provinces.find((p) => p.id === provinceId); + if (!province) return null; + const { x, y } = province.center; + const color = props.nationColors[unit.nation]; + + return ( + + + + {unit.unitType === "army" ? "A" : "F"} + + + ); + })} + {Object.entries(props.orders).map(([provinceId, order]) => { + return ( + + ); + })} + + + ); +}; + +export { InteractiveMap }; diff --git a/src/components/interactive-map/shapes/arrow.stories.tsx b/src/components/interactive-map/shapes/arrow.stories.tsx new file mode 100644 index 00000000..9a37b075 --- /dev/null +++ b/src/components/interactive-map/shapes/arrow.stories.tsx @@ -0,0 +1,60 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Arrow } from "./arrow"; + +export default { + title: "Components/Arrow", + component: Arrow, + args: { + x1: 50, + y1: 50, + x2: 200, + y2: 200, + lineWidth: 5, + fill: "blue", + stroke: "red", + strokeWidth: 1, + offset: 50, + arrowWidth: 7.5, + arrowLength: 10, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (args: any) => ( + + + + x1,y1 + + + + x2,y2 + + + + + + ), +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Dashed: Story = { + args: { + dash: { length: 10, spacing: 2.5 }, + }, +}; diff --git a/src/components/interactive-map/shapes/arrow.tsx b/src/components/interactive-map/shapes/arrow.tsx new file mode 100644 index 00000000..3cd84093 --- /dev/null +++ b/src/components/interactive-map/shapes/arrow.tsx @@ -0,0 +1,87 @@ +type ArrowProps = { + x1: number; + y1: number; + x2: number; + y2: number; + lineWidth: number; + arrowWidth: number; + arrowLength: number; + offset: number; + fill: string; + stroke: string; + strokeWidth: number; + dash?: { length: number; spacing: number }; + onRenderCenter?: (x: number, y: number, angle: number) => React.ReactElement; +}; + +const Arrow = (props: ArrowProps) => { + // Calculate the angle of the line + const angle = Math.atan2(props.y2 - props.y1, props.x2 - props.x1); + + // Calculate the offset points + const offsetX = props.offset * Math.cos(angle); + const offsetY = props.offset * Math.sin(angle); + + // Adjust start and end points by offset + const startX = props.x1 + offsetX; + const startY = props.y1 + offsetY; + + const endX = props.x2 - offsetX - props.arrowLength * Math.cos(angle); + const endY = props.y2 - offsetY - props.arrowLength * Math.sin(angle); + + const arrowStart = { + x: endX - (props.lineWidth / 2) * Math.cos(angle + Math.PI / 2), + y: endY - (props.lineWidth / 2) * Math.sin(angle + Math.PI / 2), + }; + + const arrowEnd = { + x: endX + (props.lineWidth / 2) * Math.cos(angle + Math.PI / 2), + y: endY + (props.lineWidth / 2) * Math.sin(angle + Math.PI / 2), + }; + + // Perpendicular to the line, 5 away from center base + const arrowBottomLeft = { + x: endX - props.arrowWidth * Math.cos(angle + Math.PI / 2), + y: endY - props.arrowWidth * Math.sin(angle + Math.PI / 2), + }; + + const arrowBottomRight = { + x: endX + props.arrowWidth * Math.cos(angle + Math.PI / 2), + y: endY + props.arrowWidth * Math.sin(angle + Math.PI / 2), + }; + + const arrowTop = { + x: endX + props.arrowLength * Math.cos(angle), + y: endY + props.arrowLength * Math.sin(angle), + }; + + return ( + + + + + {/* {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} */} + + ); +}; + +export { Arrow }; diff --git a/src/components/interactive-map/shapes/cross.tsx b/src/components/interactive-map/shapes/cross.tsx new file mode 100644 index 00000000..4706ae05 --- /dev/null +++ b/src/components/interactive-map/shapes/cross.tsx @@ -0,0 +1,74 @@ +import React from "react"; + +type CrossProps = { + x: number; + y: number; + width: number; + length: number; + angle: number; + fill: string; + stroke: string; + strokeWidth: number; +}; + +const Cross = ({ + x, + y, + width, + length, + angle, + fill, + stroke, + strokeWidth, +}: CrossProps) => { + // Convert angle from degrees to radians + const angleRad = (angle * Math.PI) / 180; + + // Define the points for the cross shape (moving clockwise from top) + const points = [ + // Top vertical arm + [-width / 2, -length], // Top left + [width / 2, -length], // Top right + [width / 2, -width / 2], // Inner top right + [length, -width / 2], // Right arm outer top + [length, width / 2], // Right arm outer bottom + [width / 2, width / 2], // Inner right bottom + [width / 2, length], // Bottom right + [-width / 2, length], // Bottom left + [-width / 2, width / 2], // Inner bottom left + [-length, width / 2], // Left arm outer bottom + [-length, -width / 2], // Left arm outer top + [-width / 2, -width / 2], // Inner top left + ]; + + // Function to rotate a point around origin and translate to final position + const transformPoint = (point: number[]): string => { + const [px, py] = point; + const rotatedX = px * Math.cos(angleRad) - py * Math.sin(angleRad) + x; + const rotatedY = px * Math.sin(angleRad) + py * Math.cos(angleRad) + y; + return `${rotatedX} ${rotatedY}`; + }; + + // Create path data by rotating and translating all points + const pathData = ` + M ${transformPoint(points[0])} + ${points + .slice(1) + .map((point) => `L ${transformPoint(point)}`) + .join(" ")} + Z + `; + + return ( + + + + ); +}; + +export { Cross }; diff --git a/src/components/interactive-map/shapes/curved-arrow.stories.tsx b/src/components/interactive-map/shapes/curved-arrow.stories.tsx new file mode 100644 index 00000000..6cf13020 --- /dev/null +++ b/src/components/interactive-map/shapes/curved-arrow.stories.tsx @@ -0,0 +1,77 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { CurvedArrow } from "./curved-arrow"; + +export default { + title: "Components/CurvedArrow", + component: CurvedArrow, + args: { + x1: 50, + y1: 50, + x2: 200, + y2: 200, + x3: 350, + y3: 50, + lineWidth: 5, + fill: "blue", + stroke: "red", + strokeWidth: 2, + offset: 50, + arrowWidth: 7.5, + arrowLength: 10, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (args: any) => ( + + + + x1,y1 + + + + x2,y2 + + + + x3,y3 + + + + + + ), +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Dashed: Story = { + args: { + dash: { length: 10, spacing: 2.5 }, + }, +}; + +export const Upwards: Story = { + args: { + x1: 50, + y1: 350, + x2: 200, + y2: 200, + x3: 350, + y3: 350, + }, +}; diff --git a/src/components/interactive-map/shapes/curved-arrow.tsx b/src/components/interactive-map/shapes/curved-arrow.tsx new file mode 100644 index 00000000..7c33b546 --- /dev/null +++ b/src/components/interactive-map/shapes/curved-arrow.tsx @@ -0,0 +1,121 @@ +import React from "react"; + +type CurvedArrowProps = { + x1: number; + y1: number; + x2: number; + y2: number; + x3: number; + y3: number; + offset: number; + fill: string; + lineWidth: number; + stroke: string; + strokeWidth: number; + arrowWidth: number; + arrowLength: number; + dash?: { length: number; spacing: number }; +}; + +const getOffsetPoint = ( + x1: number, + y1: number, + x2: number, + y2: number, + offset: number +) => { + const dx = x2 - x1; + const dy = y2 - y1; + const len = Math.sqrt(dx * dx + dy * dy); + return { + x: x1 + (dx / len) * offset, + y: y1 + (dy / len) * offset, + }; +}; + +const CurvedArrow: React.FC = (props) => { + const start = getOffsetPoint( + props.x1, + props.y1, + props.x3, + props.y3, + props.offset + ); + + const end = getOffsetPoint( + props.x2, + props.y2, + props.x3, + props.y3, + props.offset + props.arrowLength + ); + + // Angle between x2,y2 and x3,y3 + const endAngle = Math.atan2(props.y3 - props.y2, props.x3 - props.x2); + + const cp1 = { + x: start.x + (props.x3 - start.x) * 0.5, + y: start.y + (props.y3 - start.y) * 0.5, + }; + + const cp2 = { + x: end.x + (props.x3 - end.x) * 0.5, + y: end.y + (props.y3 - end.y) * 0.5, + }; + + const arrowStart = { + x: end.x - (props.lineWidth / 2) * Math.cos(endAngle + Math.PI / 2), + y: end.y - (props.lineWidth / 2) * Math.sin(endAngle + Math.PI / 2), + }; + + const arrowEnd = { + x: end.x + (props.lineWidth / 2) * Math.cos(endAngle + Math.PI / 2), + y: end.y + (props.lineWidth / 2) * Math.sin(endAngle + Math.PI / 2), + }; + + const arrowBottomLeft = { + x: end.x - props.arrowWidth * Math.cos(endAngle + Math.PI / 2), + y: end.y - props.arrowWidth * Math.sin(endAngle + Math.PI / 2), + }; + + const arrowBottomRight = { + x: end.x + props.arrowWidth * Math.cos(endAngle + Math.PI / 2), + y: end.y + props.arrowWidth * Math.sin(endAngle + Math.PI / 2), + }; + + const arrowTop = { + x: end.x - props.arrowLength * Math.cos(endAngle), + y: end.y - props.arrowLength * Math.sin(endAngle), + }; + + return ( + <> + + + + + ); +}; + +export { CurvedArrow }; diff --git a/src/components/interactive-map/shapes/order-arrow.tsx b/src/components/interactive-map/shapes/order-arrow.tsx new file mode 100644 index 00000000..a3618692 --- /dev/null +++ b/src/components/interactive-map/shapes/order-arrow.tsx @@ -0,0 +1,127 @@ +import { Cross } from "./cross"; + +// Ensures that arrow starts and ends at the edge of the source and target unit +const UNIT_RADIUS_OFFSET = 10; + +const CROSS_SIZE = 10; + +type MoveArrowProps = { + x1: number; + y1: number; + x2: number; + y2: number; + failed?: boolean; +}; + +type SupportArrowProps = { + x1: number; + y1: number; + x2: number; + y2: number; + x3: number; + y3: number; + failed?: boolean; +}; + +const MoveArrow = (props: MoveArrowProps) => { + const dx = props.x2 - props.x1; + const dy = props.y2 - props.y1; + + const length = Math.sqrt(dx * dx + dy * dy); + + const offsetX = (dx / length) * UNIT_RADIUS_OFFSET; + const offsetY = (dy / length) * UNIT_RADIUS_OFFSET; + + const startX = props.x1 + offsetX; + const startY = props.y1 + offsetY; + + const endX = props.x2 - offsetX; + const endY = props.y2 - offsetY; + + const midX = (startX + endX) / 2; + const midY = (startY + endY) / 2; + + const crossAngle = Math.atan2(dy, dx) * (180 / Math.PI); + + const path = `M ${startX} ${startY} L ${endX} ${endY}`; + + return ( + + + + + ); +}; + +const SupportArrow = (props: SupportArrowProps) => { + const dx = props.x2 - props.x1; + const dy = props.y2 - props.y1; + + const length = Math.sqrt(dx * dx + dy * dy); + + const offsetX = (dx / length) * UNIT_RADIUS_OFFSET; + const offsetY = (dy / length) * UNIT_RADIUS_OFFSET; + + const startX = props.x1 + offsetX; + const startY = props.y1 + offsetY; + + const endX = props.x2 - offsetX; + const endY = props.y2 - offsetY; + + // Calculate control point for the quadratic Bezier curve + const controlX = props.x3; + const controlY = props.y3; + + // Define the path using a quadratic Bezier curve + const path = `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`; + + // Calculate the midpoint of the quadratic Bezier curve + const t = 0.5; + const midX = + (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * controlX + t * t * endX; + const midY = + (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * controlY + t * t * endY; + + // Rotate the cross to be perpendicular to the line + const crossAngle = Math.atan2(dy, dx) * (180 / Math.PI); + + return ( + + + {/* */} + + ); +}; + +export { MoveArrow, SupportArrow }; diff --git a/src/components/interactive-map/shapes/parallel-curves.tsx b/src/components/interactive-map/shapes/parallel-curves.tsx new file mode 100644 index 00000000..0891ae61 --- /dev/null +++ b/src/components/interactive-map/shapes/parallel-curves.tsx @@ -0,0 +1,70 @@ +import React from "react"; + +interface ParallelCurvesProps { + x1: number; + y1: number; + x2: number; + y2: number; + x3: number; + y3: number; + lineWidth: number; + offset: number; +} + +const ParallelCurves: React.FC = ({ + x1, + y1, + x2, + y2, + x3, + y3, + lineWidth, + offset, +}) => { + // Function to compute perpendicular offset + const getOffsetPoint = ( + x: number, + y: number, + angle: number, + distance: number + ) => ({ + x: x + Math.cos(angle) * distance, + y: y + Math.sin(angle) * distance, + }); + + // Compute angles for the curve directions + const angle1 = Math.atan2(y2 - y1, x2 - x1); + const angle2 = Math.atan2(y3 - y2, x3 - x2); + + // Offset start and end points + const start1 = getOffsetPoint(x1, y1, angle1, offset); + const end1 = getOffsetPoint(x3, y3, angle2, -offset); + + // Compute perpendicular vector for parallel lines + const perpAngle = angle1 + Math.PI / 2; // Perpendicular direction + const start2 = getOffsetPoint(start1.x, start1.y, perpAngle, lineWidth); + const end2 = getOffsetPoint(end1.x, end1.y, perpAngle, lineWidth); + + return ( + + {/* First curved line */} + + {/* Parallel curved line */} + + + ); +}; + +export { ParallelCurves }; From 66e8715b1878d3ffd5adc85e5ed8bd0419818eca Mon Sep 17 00:00:00 2001 From: John McDowell Date: Wed, 12 Feb 2025 12:40:26 +0000 Subject: [PATCH 56/64] Wip --- .../interactive-map.stories.tsx | 8 +- .../interactive-map/interactive-map.tsx | 533 +++++++++--------- .../interactive-map/shapes/arrow.stories.tsx | 22 +- .../interactive-map/shapes/arrow.tsx | 6 +- .../interactive-map/shapes/cross.stories.tsx | 32 ++ .../interactive-map/shapes/cross.tsx | 107 ++-- .../shapes/curved-arrow.stories.tsx | 20 +- .../interactive-map/shapes/curved-arrow.tsx | 26 + .../shapes/octagon.stories.tsx | 50 ++ .../interactive-map/shapes/octagon.tsx | 45 ++ .../interactive-map/shapes/order-arrow.tsx | 127 ----- .../shapes/parallel-curves.tsx | 70 --- 12 files changed, 510 insertions(+), 536 deletions(-) create mode 100644 src/components/interactive-map/shapes/cross.stories.tsx create mode 100644 src/components/interactive-map/shapes/octagon.stories.tsx create mode 100644 src/components/interactive-map/shapes/octagon.tsx delete mode 100644 src/components/interactive-map/shapes/order-arrow.tsx delete mode 100644 src/components/interactive-map/shapes/parallel-curves.tsx diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index a50cc724..284492b1 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -39,13 +39,17 @@ export default { type: "move", target: "wal", aux: "edi", - outcome: "succeeded", + outcome: "failed", }, lon: { type: "support", target: "wal", aux: "lvp", - outcome: "succeeded", + outcome: "failed", + }, + edi: { + type: "hold", + outcome: "failed", }, }, }, diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index aff05886..11a30dfd 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -1,10 +1,9 @@ import React, { useState } from "react"; import { Map } from "../../common/map/map.parse.types"; -import { MoveArrow, SupportArrow } from "./shapes/order-arrow"; import { Arrow } from "./shapes/arrow"; import { Cross } from "./shapes/cross"; import { CurvedArrow } from "./shapes/curved-arrow"; -import { ParallelCurves } from "./shapes/parallel-curves"; +import { Octagon } from "./shapes/octagon"; type InteractiveMapProps = { map: Map; @@ -37,6 +36,8 @@ const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; const DEFAULT_FILL = "transparent"; const UNIT_RADIUS = 10; +const UNIT_OFFSET_X = 10; +const UNIT_OFFSET_Y = 10; const ORDER_STROKE_WIDTH = 1; const ORDER_LINE_WIDTH = 5; @@ -45,144 +46,11 @@ const ORDER_ARROW_LENGTH = 10; const ORDER_DASH_LENGTH = 5; const ORDER_DASH_SPACING = 2.5; -type InteractiveMapContextType = { - getProvince: (provinceId: string) => { - fill: string; - center: { x: number; y: number }; - unitCenter: { x: number; y: number }; - }; -}; - -const InteractiveMapContext = React.createContext< - InteractiveMapContextType | undefined ->(undefined); - -const InteractiveMapContextProvider: React.FC< - React.PropsWithChildren -> = (props) => { - const [selectedProvince, setSelectedProvince] = useState(null); - const [hoveredProvince, setHoveredProvince] = useState(null); - - const getFill = (provinceId: string) => { - const isSelected = selectedProvince === provinceId; - const isHovered = hoveredProvince === provinceId; - if (props.supplyCenters[provinceId]) { - const color = props.nationColors[props.supplyCenters[provinceId]]; - return color.replace( - /rgb(a?)\((\d+), (\d+), (\d+)(, [\d.]+)?\)/, - // "rgba($2, $3, $4, 0.4)" - `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})` - ); - } - if (selectedProvince === provinceId) return SELECTED_FILL; - if (hoveredProvince === provinceId) return HOVER_FILL; - return DEFAULT_FILL; - }; - - const getProvince = (provinceId: string) => { - const province = props.map.provinces.find((p) => p.id === provinceId); - if (!province) throw new Error(`Province ${provinceId} not found`); - return { - fill: getFill(provinceId), - center: province.center, - unitCenter: { x: province.center.x - 10, y: province.center.y - 10 }, - }; - }; - - return ( - - {props.children} - - ); -}; - -const useProvince = (provinceId: string) => { - const context = React.useContext(InteractiveMapContext); - if (!context) - throw new Error("Must be used in InteractiveMapContextProvider"); - return context.getProvince(provinceId); -}; - -const Order: React.FC<{ - type: "move" | "support" | "convoy" | "hold"; - source: string; - target: string; - aux: string; -}> = (props) => { - const source = useProvince(props.source); - const target = useProvince(props.target); - const aux = useProvince(props.aux); - - // TODO render moves after supports - if (props.type === "move") { - return ( - ( - - )} - /> - ); - } - if (props.type === "support") { - return ( - ( - // - // )} - /> - ); - } -}; +const ORDER_FAILED_CROSS_WIDTH = 4; +const ORDER_FAILED_CROSS_LENGTH = 16; +const ORDER_FAILED_CROSS_FILL = "red"; +const ORDER_FAILED_CROSS_STROKE = "black"; +const ORDER_FAILED_CROSS_STROKE_WIDTH = 2; const InteractiveMap: React.FC = (props) => { const [hoveredProvince, setHoveredProvince] = useState(null); @@ -217,159 +85,264 @@ const InteractiveMap: React.FC = (props) => { }; return ( - - - - - - - - - - - {props.map.backgroundElements.map((element, index) => ( - + + + + + + + + + + {props.map.backgroundElements.map((element, index) => ( + + + + ))} + {props.map.provinces.map((province) => { + return ( + setHoveredProvince(province.id)} + onMouseLeave={() => setHoveredProvince(null)} + onClick={() => setSelectedProvince(province.id)} + /> + {province.supplyCenter && ( + + + + + )} + + ); + })} + {props.map.provinces.map( + (province) => + province.text && ( + + {province.text.value} + + ) + )} + {props.map.borders.map((element, index) => ( + + ))} + {props.map.impassableProvinces.map((element, index) => ( + + ))} + {Object.entries(props.units).map(([provinceId, unit]) => { + const province = props.map.provinces.find((p) => p.id === provinceId); + if (!province) return null; + const { x, y } = province.center; + const color = props.nationColors[unit.nation]; + + return ( + + + + {unit.unitType === "army" ? "A" : "F"} + - ))} - {props.map.provinces.map((province) => { + ); + })} + {Object.entries(props.orders) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, order]) => order.type === "hold") + .map(([provinceId, order]) => { + const source = props.map.provinces.find((p) => p.id === provinceId); + if (!source) return null; return ( - - setHoveredProvince(province.id)} - onMouseLeave={() => setHoveredProvince(null)} - onClick={() => setSelectedProvince(province.id)} - /> - {province.supplyCenter && ( - - - - - )} - + ( + + ) + : undefined + } + /> ); })} - {props.map.provinces.map( - (province) => - province.text && ( - - {province.text.value} - - ) - )} - {props.map.borders.map((element, index) => ( - - ))} - {props.map.impassableProvinces.map((element, index) => ( - - ))} - {Object.entries(props.units).map(([provinceId, unit]) => { - const province = props.map.provinces.find((p) => p.id === provinceId); - if (!province) return null; - const { x, y } = province.center; - const color = props.nationColors[unit.nation]; - + {Object.entries(props.orders) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, order]) => order.type === "support") + .map(([provinceId, order]) => { + const source = props.map.provinces.find((p) => p.id === provinceId); + const target = props.map.provinces.find((p) => p.id === order.target); + const aux = props.map.provinces.find((p) => p.id === order.aux); + if (!source || !target || !aux) return null; return ( - - - - {unit.unitType === "army" ? "A" : "F"} - - + ( + + ) + : undefined + } + /> ); })} - {Object.entries(props.orders).map(([provinceId, order]) => { + {Object.entries(props.orders) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, order]) => order.type === "move") + .map(([provinceId, order]) => { + const source = props.map.provinces.find((p) => p.id === provinceId); + const target = props.map.provinces.find((p) => p.id === order.target); + if (!source || !target) return null; return ( - ( + + ) + : undefined + } /> ); })} - - + ); }; diff --git a/src/components/interactive-map/shapes/arrow.stories.tsx b/src/components/interactive-map/shapes/arrow.stories.tsx index 9a37b075..755db777 100644 --- a/src/components/interactive-map/shapes/arrow.stories.tsx +++ b/src/components/interactive-map/shapes/arrow.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryObj } from "@storybook/react"; import { Arrow } from "./arrow"; +import { Cross } from "./cross"; export default { - title: "Components/Arrow", + title: "Components/Shapes/Arrow", component: Arrow, args: { x1: 50, @@ -58,3 +59,22 @@ export const Dashed: Story = { dash: { length: 10, spacing: 2.5 }, }, }; + +export const Failed: Story = { + args: { + onRenderCenter: (x, y, angle) => { + return ( + + ); + }, + }, +}; diff --git a/src/components/interactive-map/shapes/arrow.tsx b/src/components/interactive-map/shapes/arrow.tsx index 3cd84093..62a9a3fc 100644 --- a/src/components/interactive-map/shapes/arrow.tsx +++ b/src/components/interactive-map/shapes/arrow.tsx @@ -29,6 +29,9 @@ const Arrow = (props: ArrowProps) => { const endX = props.x2 - offsetX - props.arrowLength * Math.cos(angle); const endY = props.y2 - offsetY - props.arrowLength * Math.sin(angle); + const centerX = (startX + endX) / 2; + const centerY = (startY + endY) / 2; + const arrowStart = { x: endX - (props.lineWidth / 2) * Math.cos(angle + Math.PI / 2), y: endY - (props.lineWidth / 2) * Math.sin(angle + Math.PI / 2), @@ -39,7 +42,6 @@ const Arrow = (props: ArrowProps) => { y: endY + (props.lineWidth / 2) * Math.sin(angle + Math.PI / 2), }; - // Perpendicular to the line, 5 away from center base const arrowBottomLeft = { x: endX - props.arrowWidth * Math.cos(angle + Math.PI / 2), y: endY - props.arrowWidth * Math.sin(angle + Math.PI / 2), @@ -79,7 +81,7 @@ const Arrow = (props: ArrowProps) => { strokeWidth={props.strokeWidth} fill={props.fill} /> - {/* {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} */} + {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} ); }; diff --git a/src/components/interactive-map/shapes/cross.stories.tsx b/src/components/interactive-map/shapes/cross.stories.tsx new file mode 100644 index 00000000..08d45db1 --- /dev/null +++ b/src/components/interactive-map/shapes/cross.stories.tsx @@ -0,0 +1,32 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Cross } from "./cross"; + +export default { + title: "Components/Shapes/Cross", + component: Cross, + args: { + x: 50, + y: 50, + width: 10, + length: 50, + angle: 45, + fill: "blue", + stroke: "red", + strokeWidth: 2, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (args: any) => ( + + + {/* Draw a dot represnting x,y */} + + + x,y + + + ), +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/src/components/interactive-map/shapes/cross.tsx b/src/components/interactive-map/shapes/cross.tsx index 4706ae05..262cf35a 100644 --- a/src/components/interactive-map/shapes/cross.tsx +++ b/src/components/interactive-map/shapes/cross.tsx @@ -1,5 +1,3 @@ -import React from "react"; - type CrossProps = { x: number; y: number; @@ -11,62 +9,65 @@ type CrossProps = { strokeWidth: number; }; -const Cross = ({ - x, - y, - width, - length, - angle, - fill, - stroke, - strokeWidth, -}: CrossProps) => { - // Convert angle from degrees to radians - const angleRad = (angle * Math.PI) / 180; +const Cross = (props: CrossProps) => { + const halfLength = props.length / 2; + const strokeWidth = props.strokeWidth; - // Define the points for the cross shape (moving clockwise from top) - const points = [ - // Top vertical arm - [-width / 2, -length], // Top left - [width / 2, -length], // Top right - [width / 2, -width / 2], // Inner top right - [length, -width / 2], // Right arm outer top - [length, width / 2], // Right arm outer bottom - [width / 2, width / 2], // Inner right bottom - [width / 2, length], // Bottom right - [-width / 2, length], // Bottom left - [-width / 2, width / 2], // Inner bottom left - [-length, width / 2], // Left arm outer bottom - [-length, -width / 2], // Left arm outer top - [-width / 2, -width / 2], // Inner top left - ]; + const x1 = props.x - halfLength; + const y1 = props.y; + const x2 = props.x + halfLength; + const y2 = props.y; - // Function to rotate a point around origin and translate to final position - const transformPoint = (point: number[]): string => { - const [px, py] = point; - const rotatedX = px * Math.cos(angleRad) - py * Math.sin(angleRad) + x; - const rotatedY = px * Math.sin(angleRad) + py * Math.cos(angleRad) + y; - return `${rotatedX} ${rotatedY}`; - }; + const x3 = props.x; + const y3 = props.y - halfLength; + const x4 = props.x; + const y4 = props.y + halfLength; - // Create path data by rotating and translating all points - const pathData = ` - M ${transformPoint(points[0])} - ${points - .slice(1) - .map((point) => `L ${transformPoint(point)}`) - .join(" ")} - Z - `; + const fillX1 = x1 + strokeWidth; + const fillX2 = x2 - strokeWidth; + const fillY3 = y3 + strokeWidth; + const fillY4 = y4 - strokeWidth; return ( - - + + + {/* Stroke */} + + + + + {/* Fill */} + + + ); }; diff --git a/src/components/interactive-map/shapes/curved-arrow.stories.tsx b/src/components/interactive-map/shapes/curved-arrow.stories.tsx index 6cf13020..071059f6 100644 --- a/src/components/interactive-map/shapes/curved-arrow.stories.tsx +++ b/src/components/interactive-map/shapes/curved-arrow.stories.tsx @@ -1,8 +1,9 @@ import { Meta, StoryObj } from "@storybook/react"; import { CurvedArrow } from "./curved-arrow"; +import { Cross } from "./cross"; export default { - title: "Components/CurvedArrow", + title: "Components/Shapes/CurvedArrow", component: CurvedArrow, args: { x1: 50, @@ -75,3 +76,20 @@ export const Upwards: Story = { y3: 350, }, }; + +export const Failed: Story = { + args: { + onRenderCenter: (x, y, angle) => ( + + ), + }, +}; diff --git a/src/components/interactive-map/shapes/curved-arrow.tsx b/src/components/interactive-map/shapes/curved-arrow.tsx index 7c33b546..98c197d2 100644 --- a/src/components/interactive-map/shapes/curved-arrow.tsx +++ b/src/components/interactive-map/shapes/curved-arrow.tsx @@ -15,6 +15,7 @@ type CurvedArrowProps = { arrowWidth: number; arrowLength: number; dash?: { length: number; spacing: number }; + onRenderCenter?: (x: number, y: number, angle: number) => React.ReactElement; }; const getOffsetPoint = ( @@ -33,6 +34,26 @@ const getOffsetPoint = ( }; }; +const getBezierPoint = ( + t: number, + p0: { x: number; y: number }, + p1: { x: number; y: number }, + p2: { x: number; y: number }, + p3: { x: number; y: number } +) => { + const x = + Math.pow(1 - t, 3) * p0.x + + 3 * Math.pow(1 - t, 2) * t * p1.x + + 3 * (1 - t) * Math.pow(t, 2) * p2.x + + Math.pow(t, 3) * p3.x; + const y = + Math.pow(1 - t, 3) * p0.y + + 3 * Math.pow(1 - t, 2) * t * p1.y + + 3 * (1 - t) * Math.pow(t, 2) * p2.y + + Math.pow(t, 3) * p3.y; + return { x, y }; +}; + const CurvedArrow: React.FC = (props) => { const start = getOffsetPoint( props.x1, @@ -88,6 +109,9 @@ const CurvedArrow: React.FC = (props) => { y: end.y - props.arrowLength * Math.sin(endAngle), }; + const centerPoint = getBezierPoint(0.5, start, cp1, cp2, end); + const centerAngleBezier = Math.atan2(cp2.y - cp1.y, cp2.x - cp1.x); + return ( <> = (props) => { strokeWidth={props.strokeWidth} fill={props.fill} /> + {props.onRenderCenter && + props.onRenderCenter(centerPoint.x, centerPoint.y, centerAngleBezier)} ); }; diff --git a/src/components/interactive-map/shapes/octagon.stories.tsx b/src/components/interactive-map/shapes/octagon.stories.tsx new file mode 100644 index 00000000..f4bf6fd0 --- /dev/null +++ b/src/components/interactive-map/shapes/octagon.stories.tsx @@ -0,0 +1,50 @@ +import { Meta, StoryObj } from "@storybook/react"; +import { Octagon } from "./octagon"; +import { Cross } from "./cross"; + +export default { + title: "Components/Shapes/Octagon", + component: Octagon, + args: { + x: 100, + y: 100, + size: 50, + fill: "transparent", + stroke: "black", + strokeWidth: 8, + }, + // eslint-disable-next-line @typescript-eslint/no-explicit-any + render: (args: any) => ( + + + {/* Draw a dot representing x,y */} + + + x,y + + + ), +} as Meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const Failed: Story = { + args: { + onRenderBottomCenter: (x: number, y: number) => { + return ( + + ); + }, + }, +}; diff --git a/src/components/interactive-map/shapes/octagon.tsx b/src/components/interactive-map/shapes/octagon.tsx new file mode 100644 index 00000000..064305d0 --- /dev/null +++ b/src/components/interactive-map/shapes/octagon.tsx @@ -0,0 +1,45 @@ +type OctagonProps = { + x: number; + y: number; + size: number; + fill: string; + stroke: string; + strokeWidth: number; + onRenderBottomCenter?: (x: number, y: number) => React.ReactElement; +}; + +const Octagon = (props: OctagonProps) => { + const angle = Math.PI / 4; + const radius = props.size / (2 * Math.sin(angle)); + const points = Array.from({ length: 8 }).map((_, i) => { + const theta = angle * i + Math.PI / 8; // Rotate by 22.5 degrees + return [ + props.x + radius * Math.cos(theta), + props.y + radius * Math.sin(theta), + ].join(","); + }); + + const bottomCenterX = + (parseFloat(points[1].split(",")[0]) + + parseFloat(points[2].split(",")[0])) / + 2; + const bottomCenterY = + (parseFloat(points[1].split(",")[1]) + + parseFloat(points[2].split(",")[1])) / + 2; + + return ( + + + {props.onRenderBottomCenter && + props.onRenderBottomCenter(bottomCenterX, bottomCenterY)} + + ); +}; + +export { Octagon }; diff --git a/src/components/interactive-map/shapes/order-arrow.tsx b/src/components/interactive-map/shapes/order-arrow.tsx deleted file mode 100644 index a3618692..00000000 --- a/src/components/interactive-map/shapes/order-arrow.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import { Cross } from "./cross"; - -// Ensures that arrow starts and ends at the edge of the source and target unit -const UNIT_RADIUS_OFFSET = 10; - -const CROSS_SIZE = 10; - -type MoveArrowProps = { - x1: number; - y1: number; - x2: number; - y2: number; - failed?: boolean; -}; - -type SupportArrowProps = { - x1: number; - y1: number; - x2: number; - y2: number; - x3: number; - y3: number; - failed?: boolean; -}; - -const MoveArrow = (props: MoveArrowProps) => { - const dx = props.x2 - props.x1; - const dy = props.y2 - props.y1; - - const length = Math.sqrt(dx * dx + dy * dy); - - const offsetX = (dx / length) * UNIT_RADIUS_OFFSET; - const offsetY = (dy / length) * UNIT_RADIUS_OFFSET; - - const startX = props.x1 + offsetX; - const startY = props.y1 + offsetY; - - const endX = props.x2 - offsetX; - const endY = props.y2 - offsetY; - - const midX = (startX + endX) / 2; - const midY = (startY + endY) / 2; - - const crossAngle = Math.atan2(dy, dx) * (180 / Math.PI); - - const path = `M ${startX} ${startY} L ${endX} ${endY}`; - - return ( - - - - - ); -}; - -const SupportArrow = (props: SupportArrowProps) => { - const dx = props.x2 - props.x1; - const dy = props.y2 - props.y1; - - const length = Math.sqrt(dx * dx + dy * dy); - - const offsetX = (dx / length) * UNIT_RADIUS_OFFSET; - const offsetY = (dy / length) * UNIT_RADIUS_OFFSET; - - const startX = props.x1 + offsetX; - const startY = props.y1 + offsetY; - - const endX = props.x2 - offsetX; - const endY = props.y2 - offsetY; - - // Calculate control point for the quadratic Bezier curve - const controlX = props.x3; - const controlY = props.y3; - - // Define the path using a quadratic Bezier curve - const path = `M ${startX} ${startY} Q ${controlX} ${controlY} ${endX} ${endY}`; - - // Calculate the midpoint of the quadratic Bezier curve - const t = 0.5; - const midX = - (1 - t) * (1 - t) * startX + 2 * (1 - t) * t * controlX + t * t * endX; - const midY = - (1 - t) * (1 - t) * startY + 2 * (1 - t) * t * controlY + t * t * endY; - - // Rotate the cross to be perpendicular to the line - const crossAngle = Math.atan2(dy, dx) * (180 / Math.PI); - - return ( - - - {/* */} - - ); -}; - -export { MoveArrow, SupportArrow }; diff --git a/src/components/interactive-map/shapes/parallel-curves.tsx b/src/components/interactive-map/shapes/parallel-curves.tsx deleted file mode 100644 index 0891ae61..00000000 --- a/src/components/interactive-map/shapes/parallel-curves.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from "react"; - -interface ParallelCurvesProps { - x1: number; - y1: number; - x2: number; - y2: number; - x3: number; - y3: number; - lineWidth: number; - offset: number; -} - -const ParallelCurves: React.FC = ({ - x1, - y1, - x2, - y2, - x3, - y3, - lineWidth, - offset, -}) => { - // Function to compute perpendicular offset - const getOffsetPoint = ( - x: number, - y: number, - angle: number, - distance: number - ) => ({ - x: x + Math.cos(angle) * distance, - y: y + Math.sin(angle) * distance, - }); - - // Compute angles for the curve directions - const angle1 = Math.atan2(y2 - y1, x2 - x1); - const angle2 = Math.atan2(y3 - y2, x3 - x2); - - // Offset start and end points - const start1 = getOffsetPoint(x1, y1, angle1, offset); - const end1 = getOffsetPoint(x3, y3, angle2, -offset); - - // Compute perpendicular vector for parallel lines - const perpAngle = angle1 + Math.PI / 2; // Perpendicular direction - const start2 = getOffsetPoint(start1.x, start1.y, perpAngle, lineWidth); - const end2 = getOffsetPoint(end1.x, end1.y, perpAngle, lineWidth); - - return ( - - {/* First curved line */} - - {/* Parallel curved line */} - - - ); -}; - -export { ParallelCurves }; From 5a998bd70b735499b3899005c68fff7c3edcbe76 Mon Sep 17 00:00:00 2001 From: Joren <7293081+JorenC@users.noreply.github.com> Date: Wed, 12 Feb 2025 18:20:13 +0100 Subject: [PATCH 57/64] Improved Cross Made the cross diagonal, better size/aspect ratio. --- .../interactive-map/interactive-map.tsx | 15 ++++++++------- .../interactive-map/shapes/curved-arrow.tsx | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 11a30dfd..489e2d6c 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -26,14 +26,15 @@ type InteractiveMapProps = { }; const HOVER_STROKE_WIDTH = 3; -const HOVER_STROKE_COLOR = "white"; +const HOVER_STROKE_COLOR = "black"; const HOVER_FILL = "rgba(255, 255, 255, 0.6)"; const SELECTED_STROKE_WIDTH = 3; -const SELECTED_STROKE_COLOR = "white"; +const SELECTED_STROKE_COLOR = "black"; const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; const DEFAULT_FILL = "transparent"; +const FAILED_COLOR = "rgba(255,0,0,0.6)"; const UNIT_RADIUS = 10; const UNIT_OFFSET_X = 10; @@ -46,8 +47,8 @@ const ORDER_ARROW_LENGTH = 10; const ORDER_DASH_LENGTH = 5; const ORDER_DASH_SPACING = 2.5; -const ORDER_FAILED_CROSS_WIDTH = 4; -const ORDER_FAILED_CROSS_LENGTH = 16; +const ORDER_FAILED_CROSS_WIDTH = 3; +const ORDER_FAILED_CROSS_LENGTH = 20; const ORDER_FAILED_CROSS_FILL = "red"; const ORDER_FAILED_CROSS_STROKE = "black"; const ORDER_FAILED_CROSS_STROKE_WIDTH = 2; @@ -236,7 +237,7 @@ const InteractiveMap: React.FC = (props) => { y={source.center.y - UNIT_OFFSET_Y} strokeWidth={ORDER_LINE_WIDTH} size={24} - stroke={"#000000"} + stroke={order.outcome === "failed" ? FAILED_COLOR : "rgba(0,0,0,0.7)"} fill={"transparent"} onRenderBottomCenter={ order.outcome === "failed" @@ -292,7 +293,7 @@ const InteractiveMap: React.FC = (props) => { y={y} width={ORDER_FAILED_CROSS_WIDTH} length={ORDER_FAILED_CROSS_LENGTH} - angle={angle} + angle={45} fill={ORDER_FAILED_CROSS_FILL} stroke={ORDER_FAILED_CROSS_STROKE} strokeWidth={ORDER_FAILED_CROSS_STROKE_WIDTH} @@ -331,7 +332,7 @@ const InteractiveMap: React.FC = (props) => { y={y} width={ORDER_FAILED_CROSS_WIDTH} length={ORDER_FAILED_CROSS_LENGTH} - angle={angle} + angle={45} fill={ORDER_FAILED_CROSS_FILL} stroke={ORDER_FAILED_CROSS_STROKE} strokeWidth={ORDER_FAILED_CROSS_STROKE_WIDTH} diff --git a/src/components/interactive-map/shapes/curved-arrow.tsx b/src/components/interactive-map/shapes/curved-arrow.tsx index 98c197d2..d10d3e49 100644 --- a/src/components/interactive-map/shapes/curved-arrow.tsx +++ b/src/components/interactive-map/shapes/curved-arrow.tsx @@ -109,7 +109,7 @@ const CurvedArrow: React.FC = (props) => { y: end.y - props.arrowLength * Math.sin(endAngle), }; - const centerPoint = getBezierPoint(0.5, start, cp1, cp2, end); + const centerPoint = getBezierPoint(0.3, start, cp1, cp2, end); const centerAngleBezier = Math.atan2(cp2.y - cp1.y, cp2.x - cp1.x); return ( From 4e220831d1530e16e8805e58afe9f8cbbd801307 Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 19:46:57 +0100 Subject: [PATCH 58/64] Adding units --- .../interactive-map.stories.tsx | 91 +++++++++++++++++-- .../interactive-map/interactive-map.tsx | 28 +++--- 2 files changed, 97 insertions(+), 22 deletions(-) diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index 284492b1..c681265c 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -8,31 +8,104 @@ export default { args: { map: classical, units: { - lon: { + edi: { + unitType: "fleet", + nation: "England", + }, + hol: { unitType: "army", nation: "England", }, - lvp: { + den: { + unitType: "army", + nation: "England", + }, + bot: { unitType: "fleet", nation: "England", }, - edi: { + spa: { + unitType: "army", + nation: "France", + }, + gol: { + unitType: "fleet", + nation: "France", + }, + wes: { + unitType: "fleet", + nation: "France", + }, + tun: { + unitType: "fleet", + nation: "France", + }, + tus: { + unitType: "army", + nation: "France", + }, + gal: { + unitType: "army", + nation: "France", + }, + boh: { + unitType: "army", + nation: "France", + }, + sil: { unitType: "army", + nation: "Germany", + }, + mun: { + unitType: "army", + nation: "Germany", + }, + bot: { + unitType: "fleet", nation: "England", }, }, nationColors: { - England: "rgb(255, 0, 0)", - France: "rgb(0, 0, 255)", - Germany: "rgb(0, 255, 0)", - Italy: "rgb(255, 255, 0)", - Austria: "rgb(255, 0, 255)", - Russia: "rgb(0, 255, 255)", + England: "rgb(33, 150, 243)", + France: "rgb(128, 222, 234)", + Germany: "rgb(144, 164, 174)", + Italy: "rgb(76, 175, 80)", + Austria: "rgb(244, 67, 54)", + Russia: "rgb(245, 245, 245)", + Turkey: "rbg(255, 193, 7)", }, supplyCenters: { lon: "England", lvp: "England", edi: "England", + nor: "England", + swe: "England", + stp: "Russia", + mos: "Russia", + spa: "France", + bre: "France", + par: "France", + mar: "France", + tun: "France", + bel: "France", + hol: "France", + kie: "France", + ber: "France", + war: "Germany", + mun: "Germany", + ven: "Italy", + nap: "Italy", + gre: "Italy", + rom: "Italy", + vie: "Austria", + bud: "Austria", + tri: "Austria", + ser: "Austria", + rum: "Turkey", + bul: "Turkey", + con: "Turkey", + smy: "Turkey", + sev: "Turkey", }, orders: { lvp: { diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 489e2d6c..56ef79cd 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -34,18 +34,20 @@ const SELECTED_STROKE_COLOR = "black"; const SELECTED_FILL = "rgba(255, 255, 255, 0.8)"; const DEFAULT_FILL = "transparent"; +const SUCCESS_COLOR = "rgba(0,0,0,1)"; const FAILED_COLOR = "rgba(255,0,0,0.6)"; const UNIT_RADIUS = 10; +const UNIT_OFFSET_RADIUS = 3; const UNIT_OFFSET_X = 10; const UNIT_OFFSET_Y = 10; -const ORDER_STROKE_WIDTH = 1; -const ORDER_LINE_WIDTH = 5; -const ORDER_ARROW_WIDTH = 7.5; -const ORDER_ARROW_LENGTH = 10; -const ORDER_DASH_LENGTH = 5; -const ORDER_DASH_SPACING = 2.5; +const ORDER_STROKE_WIDTH = 2.5; +const ORDER_LINE_WIDTH = 3; +const ORDER_ARROW_WIDTH = 6; +const ORDER_ARROW_LENGTH = 8; +const ORDER_DASH_LENGTH = 4; +const ORDER_DASH_SPACING = 2; const ORDER_FAILED_CROSS_WIDTH = 3; const ORDER_FAILED_CROSS_LENGTH = 20; @@ -65,7 +67,7 @@ const InteractiveMap: React.FC = (props) => { return color.replace( /rgb(a?)\((\d+), (\d+), (\d+)(, [\d.]+)?\)/, // "rgba($2, $3, $4, 0.4)" - `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})` + `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})`, ); } if (selectedProvince === provinceId) return SELECTED_FILL; @@ -176,7 +178,7 @@ const InteractiveMap: React.FC = (props) => { > {province.text.value} - ) + ), )} {props.map.borders.map((element, index) => ( = (props) => { y={source.center.y - UNIT_OFFSET_Y} strokeWidth={ORDER_LINE_WIDTH} size={24} - stroke={order.outcome === "failed" ? FAILED_COLOR : "rgba(0,0,0,0.7)"} + stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} fill={"transparent"} onRenderBottomCenter={ order.outcome === "failed" @@ -278,8 +280,8 @@ const InteractiveMap: React.FC = (props) => { arrowWidth={ORDER_ARROW_WIDTH} arrowLength={ORDER_ARROW_LENGTH} strokeWidth={ORDER_STROKE_WIDTH} - offset={UNIT_RADIUS} - stroke={"#000000"} + offset={UNIT_RADIUS + UNIT_OFFSET_RADIUS} + stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} fill={"green"} dash={{ length: ORDER_DASH_LENGTH, @@ -321,8 +323,8 @@ const InteractiveMap: React.FC = (props) => { arrowWidth={ORDER_ARROW_WIDTH} arrowLength={ORDER_ARROW_LENGTH} strokeWidth={ORDER_STROKE_WIDTH} - offset={UNIT_RADIUS} - stroke={"#000000"} + offset={UNIT_RADIUS + UNIT_OFFSET_RADIUS} + stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} fill={"green"} onRenderCenter={ order.outcome === "failed" From 3ea2fd5926f2aa92809a52f1139738b455ea9c27 Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 20:54:37 +0100 Subject: [PATCH 59/64] Arrows now work - Added many stories to a realistic map (this is real game) - Arrows are now the colour of their origin unit --- .../interactive-map.stories.tsx | 214 ++++++++++++++++-- .../interactive-map/interactive-map.tsx | 8 +- 2 files changed, 206 insertions(+), 16 deletions(-) diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index c681265c..8df99b14 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -48,10 +48,6 @@ export default { unitType: "army", nation: "France", }, - boh: { - unitType: "army", - nation: "France", - }, sil: { unitType: "army", nation: "Germany", @@ -60,9 +56,77 @@ export default { unitType: "army", nation: "Germany", }, - bot: { + pie: { unitType: "fleet", - nation: "England", + nation: "Turkey", + }, + ven: { + unitType: "army", + nation: "Italy", + }, + rom: { + unitType: "army", + nation: "Italy", + }, + nap: { + unitType: "fleet", + nation: "Italy", + }, + apu: { + unitType: "army", + nation: "Austria", + }, + adr: { + unitType: "fleet", + nation: "Italy", + }, + tys: { + unitType: "fleet", + nation: "Turkey", + }, + bur: { + unitType: "army", + nation: "France", + }, + ion: { + unitType: "fleet", + nation: "Turkey", + }, + ank: { + unitType: "army", + nation: "Turkey", + }, + bud: { + unitType: "army", + nation: "Austria", + }, + vie: { + unitType: "army", + nation: "Austria", + }, + tyr: { + unitType: "army", + nation: "Austria", + }, + boh: { + unitType: "army", + nation: "France", + }, + sev: { + unitType: "army", + nation: "Turkey", + }, + ukr: { + unitType: "army", + nation: "Russia", + }, + mos: { + unitType: "army", + nation: "France", + }, + war: { + unitType: "army", + nation: "Russia", }, }, nationColors: { @@ -72,7 +136,7 @@ export default { Italy: "rgb(76, 175, 80)", Austria: "rgb(244, 67, 54)", Russia: "rgb(245, 245, 245)", - Turkey: "rbg(255, 193, 7)", + Turkey: "rgb(255, 193, 7)", }, supplyCenters: { lon: "England", @@ -83,6 +147,7 @@ export default { stp: "Russia", mos: "Russia", spa: "France", + bur: "France", bre: "France", par: "France", mar: "France", @@ -108,19 +173,140 @@ export default { sev: "Turkey", }, orders: { - lvp: { + den: { type: "move", - target: "wal", - aux: "edi", + target: "swe", + outcome: "success", + }, + hol: { + type: "move", + target: "kie", + outcome: "success", + }, + bur: { + type: "move", + target: "mar", + outcome: "success", + }, + pie: { + type: "move", + target: "gol", + outcome: "success", + }, + gol: { + type: "support", + target: "pie", + aux: "tus", + outcome: "success", + }, + tus: { + type: "move", + target: "pie", outcome: "failed", }, - lon: { + rom: { + type: "move", + target: "tus", + outcome: "success", + }, + ven: { type: "support", - target: "wal", - aux: "lvp", + target: "tus", + aux: "rom", + outcome: "success", + }, + tun: { + type: "hold", + outcome: "success", + }, + tys: { + type: "move", + target: "tun", outcome: "failed", }, - edi: { + ion: { + type: "support", + target: "tun", + aux: "tys", + outcome: "success", + }, + nap: { + type: "move", + target: "tys", + outcome: "failed", + }, + apu: { + type: "move", + target: "tri", + outcome: "success", + }, + spa: { + type: "hold", + outcome: "success", + }, + ank: { + type: "hold", + outcome: "success", + }, + mun: { + type: "move", + target: "tyr", + outcome: "success", + }, + boh: { + type: "support", + target: "tyr", + aux: "mun", + outcome: "failed", + }, + vie: { + type: "support", + target: "boh", + aux: "tyr", + outcome: "success", + }, + tyr: { + type: "move", + target: "boh", + outcome: "success", + }, + pru: { + type: "move", + target: "war", + outcome: "failed", + }, + sil: { + type: "move", + target: "war", + outcome: "failed", + }, + war: { + type: "move", + target: "gal", + outcome: "failed", + }, + gal: { + type: "move", + target: "rum", + outcome: "success", + }, + bud: { + type: "move", + target: "gal", + outcome: "failed", + }, + ukr: { + type: "support", + target: "mos", + aux: "sev", + outcome: "success", + }, + sev: { + type: "move", + target: "mos", + outcome: "success", + }, + mos: { type: "hold", outcome: "failed", }, diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 56ef79cd..3b98ff1b 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -266,6 +266,8 @@ const InteractiveMap: React.FC = (props) => { .map(([provinceId, order]) => { const source = props.map.provinces.find((p) => p.id === provinceId); const target = props.map.provinces.find((p) => p.id === order.target); + const unit = props.units[provinceId]; + const unitColor = unit ? props.nationColors[unit.nation] : "black"; // Default to black if no unit found const aux = props.map.provinces.find((p) => p.id === order.aux); if (!source || !target || !aux) return null; return ( @@ -282,7 +284,7 @@ const InteractiveMap: React.FC = (props) => { strokeWidth={ORDER_STROKE_WIDTH} offset={UNIT_RADIUS + UNIT_OFFSET_RADIUS} stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} - fill={"green"} + fill={unitColor} dash={{ length: ORDER_DASH_LENGTH, spacing: ORDER_DASH_SPACING, @@ -312,6 +314,8 @@ const InteractiveMap: React.FC = (props) => { .map(([provinceId, order]) => { const source = props.map.provinces.find((p) => p.id === provinceId); const target = props.map.provinces.find((p) => p.id === order.target); + const unit = props.units[provinceId]; + const unitColor = unit ? props.nationColors[unit.nation] : "black"; // Default to black if no unit found if (!source || !target) return null; return ( = (props) => { strokeWidth={ORDER_STROKE_WIDTH} offset={UNIT_RADIUS + UNIT_OFFSET_RADIUS} stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} - fill={"green"} + fill={unitColor} onRenderCenter={ order.outcome === "failed" ? (x, y, angle) => ( From 1ae9db64fc3f76a7ef1509895e6ca9a536ca1690 Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 21:49:57 +0100 Subject: [PATCH 60/64] Added support order --- .../interactive-map.stories.tsx | 6 ++ .../interactive-map/interactive-map.tsx | 28 +++++- .../interactive-map/shapes/hold-arrow.tsx | 87 +++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 src/components/interactive-map/shapes/hold-arrow.tsx diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index 8df99b14..3b605f53 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -193,6 +193,12 @@ export default { target: "gol", outcome: "success", }, + wes: { + type: "support", + target: "tun", + aux: "tun", + outcome: "failed", + }, gol: { type: "support", target: "pie", diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 3b98ff1b..4e4c2f9c 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -4,6 +4,7 @@ import { Arrow } from "./shapes/arrow"; import { Cross } from "./shapes/cross"; import { CurvedArrow } from "./shapes/curved-arrow"; import { Octagon } from "./shapes/octagon"; +import { HoldArrow } from "./shapes/hold-arrow"; type InteractiveMapProps = { map: Map; @@ -38,7 +39,7 @@ const SUCCESS_COLOR = "rgba(0,0,0,1)"; const FAILED_COLOR = "rgba(255,0,0,0.6)"; const UNIT_RADIUS = 10; -const UNIT_OFFSET_RADIUS = 3; +const UNIT_OFFSET_RADIUS = 5; const UNIT_OFFSET_X = 10; const UNIT_OFFSET_Y = 10; @@ -270,6 +271,31 @@ const InteractiveMap: React.FC = (props) => { const unitColor = unit ? props.nationColors[unit.nation] : "black"; // Default to black if no unit found const aux = props.map.provinces.find((p) => p.id === order.aux); if (!source || !target || !aux) return null; + + if (order.aux === order.target) { + // Render HoldArrow if auxiliary is the same as the target + return ( + + ); + } return ( React.ReactElement; +}; + +const HoldArrow = (props: ArrowProps) => { + // Calculate the angle of the line + const angle = Math.atan2(props.y2 - props.y1, props.x2 - props.x1); + + // Calculate the offset points + const offsetX = props.offset * Math.cos(angle); + const offsetY = props.offset * Math.sin(angle); + + // Adjust start and end points by offset + const startX = props.x1 + offsetX; + const startY = props.y1 + offsetY; + + const endX = props.x2 - offsetX - Math.cos(angle); + const endY = props.y2 - offsetY - Math.sin(angle); + + const centerX = (startX + endX) / 2; + const centerY = (startY + endY) / 2; + + //from here + const arrowStart = { + x: endX - (props.lineWidth / 2) * Math.cos(angle + Math.PI / 2), + y: endY - (props.lineWidth / 2) * Math.sin(angle + Math.PI / 2), + }; + + const arrowEnd = { + x: endX + props.lineWidth * Math.cos(angle + Math.PI / 2), + y: endY + props.lineWidth * Math.sin(angle + Math.PI / 2), + }; + + const arrowBottomLeft = { + x: endX - props.arrowWidth * Math.cos(angle + Math.PI / 2), + y: endY - props.arrowWidth * Math.sin(angle + Math.PI / 2), + }; + + const arrowBottomRight = { + x: endX + props.arrowWidth * Math.cos(angle + Math.PI / 2), + y: endY + props.arrowWidth * Math.sin(angle + Math.PI / 2), + }; + + const arrowTop = { + x: endX + props.arrowLength * Math.cos(angle), + y: endY + props.arrowLength * Math.sin(angle), + }; + //to here + return ( + + + + {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} + + + ); +}; + +export { HoldArrow }; From e998d9e6a36e9dedd439f05d5158ac559891077f Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 21:53:52 +0100 Subject: [PATCH 61/64] Update SC rendering Much better to see --- src/components/interactive-map/interactive-map.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 4e4c2f9c..4e4d172e 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -147,18 +147,18 @@ const InteractiveMap: React.FC = (props) => { )} From f39ad4d3b7937b475ce44e0123af8b3d76e34612 Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 22:18:13 +0100 Subject: [PATCH 62/64] Better Octagon colour colour updated for the support octagon --- .../interactive-map/interactive-map.stories.tsx | 6 +++++- src/components/interactive-map/shapes/hold-arrow.tsx | 9 ++++++++- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index 3b605f53..2b7cb37c 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -128,6 +128,10 @@ export default { unitType: "army", nation: "Russia", }, + pru: { + unitType: "army", + nation: "England", + }, }, nationColors: { England: "rgb(33, 150, 243)", @@ -197,7 +201,7 @@ export default { type: "support", target: "tun", aux: "tun", - outcome: "failed", + outcome: "success", }, gol: { type: "support", diff --git a/src/components/interactive-map/shapes/hold-arrow.tsx b/src/components/interactive-map/shapes/hold-arrow.tsx index 05e03054..3857f226 100644 --- a/src/components/interactive-map/shapes/hold-arrow.tsx +++ b/src/components/interactive-map/shapes/hold-arrow.tsx @@ -79,7 +79,14 @@ const HoldArrow = (props: ArrowProps) => { } /> {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} - + ); }; From c9ac42c2d4d2aa5b052540970dea16b285e9e872 Mon Sep 17 00:00:00 2001 From: Joren Date: Wed, 12 Feb 2025 23:05:26 +0100 Subject: [PATCH 63/64] Added Convoy Arrow --- .../interactive-map.stories.tsx | 6 + .../interactive-map/interactive-map.tsx | 53 +++++++- .../interactive-map/shapes/convoy-arrow.tsx | 116 ++++++++++++++++++ 3 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 src/components/interactive-map/shapes/convoy-arrow.tsx diff --git a/src/components/interactive-map/interactive-map.stories.tsx b/src/components/interactive-map/interactive-map.stories.tsx index 2b7cb37c..5ebdde48 100644 --- a/src/components/interactive-map/interactive-map.stories.tsx +++ b/src/components/interactive-map/interactive-map.stories.tsx @@ -219,6 +219,12 @@ export default { target: "tus", outcome: "success", }, + adr: { + type: "convoy", + target: "tri", + aux: "apu", + outcome: "success", + }, ven: { type: "support", target: "tus", diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 4e4d172e..35f58b79 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -5,6 +5,7 @@ import { Cross } from "./shapes/cross"; import { CurvedArrow } from "./shapes/curved-arrow"; import { Octagon } from "./shapes/octagon"; import { HoldArrow } from "./shapes/hold-arrow"; +import { ConvoyArrow } from "./shapes/convoy-arrow"; type InteractiveMapProps = { map: Map; @@ -156,7 +157,7 @@ const InteractiveMap: React.FC = (props) => { cx={province.center.x} cy={province.center.y} r={4} - fill="none" + fill="white" stroke="black" strokeWidth={2} /> @@ -334,6 +335,7 @@ const InteractiveMap: React.FC = (props) => { /> ); })} + {Object.entries(props.orders) // eslint-disable-next-line @typescript-eslint/no-unused-vars .filter(([_, order]) => order.type === "move") @@ -375,6 +377,55 @@ const InteractiveMap: React.FC = (props) => { /> ); })} + {Object.entries(props.orders) + // eslint-disable-next-line @typescript-eslint/no-unused-vars + .filter(([_, order]) => order.type === "convoy") + .map(([provinceId, order]) => { + const source = props.map.provinces.find((p) => p.id === provinceId); + const target = props.map.provinces.find((p) => p.id === order.target); + const unit = props.units[provinceId]; + const unitColor = unit ? props.nationColors[unit.nation] : "black"; // Default to black if no unit found + const aux = props.map.provinces.find((p) => p.id === order.aux); + if (!source || !target || !aux) return null; + + return ( + ( + + ) + : undefined + } + /> + ); + })} ); }; diff --git a/src/components/interactive-map/shapes/convoy-arrow.tsx b/src/components/interactive-map/shapes/convoy-arrow.tsx new file mode 100644 index 00000000..78909291 --- /dev/null +++ b/src/components/interactive-map/shapes/convoy-arrow.tsx @@ -0,0 +1,116 @@ +type ConvoyArrowProps = { + x1: number; + y1: number; + x2: number; + y2: number; + x3: number; + y3: number; + lineWidth: number; + arrowWidth: number; + arrowLength: number; + offset: number; + fill: string; + stroke: string; + strokeWidth: number; + dash?: { length: number; spacing: number }; + onRenderCenter?: (x: number, y: number, angle: number) => React.ReactElement; +}; + +const ConvoyArrow = (props: ConvoyArrowProps) => { + // Helper function to compute the projection of point (x1, y1) onto the line (x2, y2) -> (x3, y3) + const getClosestPointOnLine = ( + x1: number, + y1: number, + x2: number, + y2: number, + x3: number, + y3: number, + ) => { + // Line vector (x2, y2) -> (x3, y3) + const lineVecX = x3 - x2; + const lineVecY = y3 - y2; + + // Point vector (x2, y2) -> (x1, y1) + const pointVecX = x1 - x2; + const pointVecY = y1 - y2; + + // Project point onto the line + const lineLengthSquared = lineVecX * lineVecX + lineVecY * lineVecY; + const projection = + (pointVecX * lineVecX + pointVecY * lineVecY) / lineLengthSquared; + + // Closest point on the line + const closestX = x2 + projection * lineVecX; + const closestY = y2 + projection * lineVecY; + + return { x: closestX, y: closestY }; + }; + + // Get the closest point on the line from (x1, y1) to the line defined by (x2, y2) -> (x3, y3) + const closestPoint = getClosestPointOnLine( + props.x1, + props.y1, + props.x2, + props.y2, + props.x3, + props.y3, + ); + + // Calculate the direction vector from (x1, y1) to the closest point + const directionX = closestPoint.x - props.x1; + const directionY = closestPoint.y - props.y1; + + // Normalize the direction vector + const magnitude = Math.sqrt( + directionX * directionX + directionY * directionY, + ); + const unitX = directionX / magnitude; + const unitY = directionY / magnitude; + + // Apply the offset in the direction of the unit vector + const offsetX = props.offset * unitX; + const offsetY = props.offset * unitY; + + // Adjust the start and end points by the offset + const startX = props.x1 + offsetX; + const startY = props.y1 + offsetY; + const endX = closestPoint.x; + const endY = closestPoint.y; + + const angle = Math.atan2(endY - startY, endX - startX); + + const centerX = (startX + endX) / 2; + const centerY = (startY + endY) / 2; + + return ( + + + + {props.onRenderCenter && props.onRenderCenter(centerX, centerY, angle)} + + + ); +}; + +export { ConvoyArrow }; From fb1dc585f83af150246244336b0038dd87732406 Mon Sep 17 00:00:00 2001 From: Joren <7293081+JorenC@users.noreply.github.com> Date: Fri, 14 Feb 2025 08:42:49 +0100 Subject: [PATCH 64/64] Tokenize values Tokenized the values. I'll try to keep this best practice - still not in my system as I design using the interface and then just believe that should be the value :D --- .../interactive-map/interactive-map.tsx | 34 +++++++++++++------ 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/components/interactive-map/interactive-map.tsx b/src/components/interactive-map/interactive-map.tsx index 35f58b79..5584c84a 100644 --- a/src/components/interactive-map/interactive-map.tsx +++ b/src/components/interactive-map/interactive-map.tsx @@ -30,6 +30,9 @@ type InteractiveMapProps = { const HOVER_STROKE_WIDTH = 3; const HOVER_STROKE_COLOR = "black"; const HOVER_FILL = "rgba(255, 255, 255, 0.6)"; +const HOVER_SELECTED_TRANSPARENCY = 0.3; +const HOVER_DEFAULT_TRANSPARENCY = 0.4; +const HOVER_HOVERING_TRANSPARENCY = 0.5; const SELECTED_STROKE_WIDTH = 3; const SELECTED_STROKE_COLOR = "black"; @@ -44,12 +47,21 @@ const UNIT_OFFSET_RADIUS = 5; const UNIT_OFFSET_X = 10; const UNIT_OFFSET_Y = 10; +const SUPPLY_CENTER_SIZE_INNER = 4; +const SUPPLY_CENTER_SIZE_OUTER = 7; +const SUPPLY_CENTER_FILL = "white"; +const SUPPLY_CENTER_STROKE = "black"; +const SUPPLY_CENTER_STROKE_WIDTH = 2; + + const ORDER_STROKE_WIDTH = 2.5; const ORDER_LINE_WIDTH = 3; const ORDER_ARROW_WIDTH = 6; const ORDER_ARROW_LENGTH = 8; const ORDER_DASH_LENGTH = 4; const ORDER_DASH_SPACING = 2; +const ORDER_CONVOY_DASH_LENGTH = 8; +const ORDER_CONVOY_DASH_SPACING = 2; const ORDER_FAILED_CROSS_WIDTH = 3; const ORDER_FAILED_CROSS_LENGTH = 20; @@ -69,7 +81,7 @@ const InteractiveMap: React.FC = (props) => { return color.replace( /rgb(a?)\((\d+), (\d+), (\d+)(, [\d.]+)?\)/, // "rgba($2, $3, $4, 0.4)" - `rgba($2, $3, $4, ${isSelected ? 0.3 : isHovered ? 0.4 : 0.5})`, + `rgba($2, $3, $4, ${isSelected ? HOVER_SELECTED_TRANSPARENCY : isHovered ? HOVER_HOVERING_TRANSPARENCY : HOVER_DEFAULT_TRANSPARENCY})`, ); } if (selectedProvince === provinceId) return SELECTED_FILL; @@ -148,18 +160,18 @@ const InteractiveMap: React.FC = (props) => { )} @@ -404,8 +416,8 @@ const InteractiveMap: React.FC = (props) => { stroke={order.outcome === "failed" ? FAILED_COLOR : SUCCESS_COLOR} fill={unitColor} dash={{ - length: ORDER_DASH_LENGTH * 2, - spacing: ORDER_DASH_LENGTH, + length: ORDER_CONVOY_DASH_LENGTH, + spacing: ORDER_DASH_SPACING, }} onRenderCenter={ order.outcome === "failed"