diff --git a/.netlify/config/_headers b/.netlify/config/_headers index 8e3b576fe..a2d6e0272 100644 --- a/.netlify/config/_headers +++ b/.netlify/config/_headers @@ -2,5 +2,5 @@ X-XSS-Protection: 1; mode=block X-Frame-Options: SAMEORIGIN X-Content-Type-Options: nosniff - Content-Security-Policy: default-src https: 'unsafe-inline' + Referrer-Policy: no-referrer-when-downgrade Feature-Policy: accelerometer 'none'; camera 'none'; geolocation 'none'; gyroscope 'none'; magnetometer 'none'; microphone 'none'; payment 'none'; usb 'none' diff --git a/index.html b/index.html index f05ea74bd..ba84d08c3 100644 --- a/index.html +++ b/index.html @@ -51,6 +51,7 @@ property="og:locale:alternate" content="ru_RU" /> + /dev/null fi if [[ -z $(command -v json-linter) ]]; then diff --git a/src/@types/globals.d.ts b/src/@types/globals.d.ts index 0db5d592c..e1cad133f 100644 --- a/src/@types/globals.d.ts +++ b/src/@types/globals.d.ts @@ -1,2 +1,7 @@ declare const DB_BUILD_TIME: number; +declare const DB_ENABLE_ADS: boolean; declare const DB_DEVMODE: boolean; +declare const DB_DISPLAY_AD_PLACEHOLDERS: boolean; +declare const DB_GA4_MEASUREMENT_ID: string | null; +declare const DB_PW_PUBLISHER_ID: string | null; +declare const DB_PW_WEBSITE_ID: string | null; diff --git a/src/@types/playwire.d.ts b/src/@types/playwire.d.ts new file mode 100644 index 000000000..d37be72f6 --- /dev/null +++ b/src/@types/playwire.d.ts @@ -0,0 +1,19 @@ +interface PlaywireUnit { + type: string; + selectorId?: string; +} + +interface Window { + ramp: { + que: unknown[]; + passiveMode: boolean; + addUnits: (units: PlaywireUnit[]) => Promise; + displayUnits: () => void; + destroyUnits: (what: string) => Promise; + processPage: (path: string) => void; + }; + + _pwGA4PageviewId: string; + dataLayer: unknown[]; + gtag: (...args: unknown[]) => void; +} diff --git a/src/app.tsx b/src/app.tsx index f44b50e18..dc8a8eb03 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -22,8 +22,6 @@ import BackgroundTasks from "@src/components/BackgroundTasks"; import Favorites from "@src/pages/favorites/Favorites"; import useIsMobile from "@src/hooks/is-mobile"; import log from "@src/utils/logger"; -import SomethingWentWrong from "@src/components/SomethingWentWrong"; -import { ErrorBoundary } from "react-error-boundary"; import useIsLightMode from "@src/hooks/light-mode"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; import { isRtlLanguage, Language } from "@src/i18n"; @@ -33,6 +31,10 @@ import rtlPlugin from "stylis-plugin-rtl"; import { prefixer } from "stylis"; import { CacheProvider } from "@emotion/react"; import createCache from "@emotion/cache"; +import { ErrorBoundary } from "react-error-boundary"; +import SomethingWentWrong from "@src/components/SometingWentWrong"; +import Privacy from "./pages/about/Privacy"; +import TrackingRampSetup from "@src/components/TrackingRampSetup"; const DauntlessBuilderApp = () => { const isMobile = useIsMobile(); @@ -66,6 +68,7 @@ const DauntlessBuilderApp = () => { > + { path="/about" /> + } + path="/privacy" + /> + } path="/settings" diff --git a/src/components/AdSpace.tsx b/src/components/AdSpace.tsx new file mode 100644 index 000000000..9b9a5e9cf --- /dev/null +++ b/src/components/AdSpace.tsx @@ -0,0 +1,110 @@ +import { FeaturedVideo } from "@mui/icons-material"; +import { Box, useTheme } from "@mui/material"; +import { eventsAtom, playwireAddInitializedUnit } from "@src/state/events"; +import { adsEnabled } from "@src/utils/env-tools"; +import log from "@src/utils/logger"; +import { useAtom } from "jotai"; +import md5 from "md5"; +import React, { useEffect } from "react"; + +export enum UnitType { + BottomRail = "bottom_rail", + Skyscraper = "sky_atf", + MediumRect = "med_rect_atf", + RightRail = "right_rail", +} + +export const adSpaceSize = { + [UnitType.BottomRail]: { height: 50, width: 320 }, + [UnitType.Skyscraper]: { height: 600, width: 160 }, + [UnitType.MediumRect]: { height: 250, width: 300 }, + [UnitType.RightRail]: { height: 600, width: 120 }, +}; + +interface AdSpaceProps { + name: string; + unitType: UnitType; +} + +const AdSpace: React.FC = ({ name, unitType }) => { + const theme = useTheme(); + + const [events, setEvents] = useAtom(eventsAtom); + + const selectorName = `dbu_${md5(name + unitType.toString())}`; + + const size = unitType in adSpaceSize ? adSpaceSize[unitType as keyof typeof adSpaceSize] : null; + + useEffect(() => { + if (events.playwireInitializedUnits.indexOf(name) > -1) { + log.error(`ramp: Unit ${name} (${unitType}) has already been initialized...`); + return; + } + + if (DB_DISPLAY_AD_PLACEHOLDERS) { + setEvents(playwireAddInitializedUnit(name)); + log.debug(`not ramp: initialized unit ${name} (${unitType})`); + } + + if (!adsEnabled) { + return; + } + + if (!events.playwireSetupHasFinished) { + return; + } + + const initUnit = async () => { + try { + await window.ramp.addUnits([ + { + selectorId: selectorName, + type: unitType, + }, + ]); + window.ramp.displayUnits(); + } catch (error) { + log.error("ramp: could not add unit", { error }); + window.ramp.displayUnits(); + } + + setEvents(playwireAddInitializedUnit(name)); + log.debug(`ramp: initialized unit ${name} (${unitType})`); + }; + initUnit(); + + return () => { + window.ramp.destroyUnits(selectorName).then(() => window.ramp.processPage(window.location.pathname)); + }; + }, [events.playwireSetupHasFinished, events.playwireInitializedUnits, selectorName, unitType, setEvents, name]); + + if (DB_DISPLAY_AD_PLACEHOLDERS) { + return ( + + + + ); + } + + if (!adsEnabled) { + return null; + } + + return
; +}; + +export default React.memo(AdSpace); diff --git a/src/components/AdSpaceFloating.tsx b/src/components/AdSpaceFloating.tsx new file mode 100644 index 000000000..a39cbc3c1 --- /dev/null +++ b/src/components/AdSpaceFloating.tsx @@ -0,0 +1,65 @@ +import { Box, useTheme } from "@mui/material"; +import { Breakpoint } from "@mui/system"; +import AdSpace, { UnitType } from "@src/components/AdSpace"; +import useWindowSize from "@src/hooks/window-size"; +import { adsEnabled } from "@src/utils/env-tools"; +import React from "react"; + +interface AdSpaceFloatingProps { + name: string; + unitType: UnitType; + fromBreakpoint?: Breakpoint; + untilBreakpoint?: Breakpoint; + left?: number; + right?: number; + top?: number; + bottom?: number; +} + +const AdSpaceFloating: React.FC = ({ + name, + unitType, + fromBreakpoint, + untilBreakpoint, + left, + right, + top, + bottom, +}) => { + const theme = useTheme(); + const { width } = useWindowSize(); + + if (!adsEnabled && !DB_DISPLAY_AD_PLACEHOLDERS) { + return null; + } + + const canRenderUsingFromRule = fromBreakpoint ? width >= theme.breakpoints.values[fromBreakpoint] : true; + const canRenderUsingUntilRule = untilBreakpoint ? width <= theme.breakpoints.values[untilBreakpoint] : true; + + if (!canRenderUsingFromRule || !canRenderUsingUntilRule) { + return null; + } + + return ( + + + + ); +}; + +export default AdSpaceFloating; diff --git a/src/components/BuildMenu.tsx b/src/components/BuildMenu.tsx index 009887bab..1b93103d9 100644 --- a/src/components/BuildMenu.tsx +++ b/src/components/BuildMenu.tsx @@ -1,8 +1,11 @@ import { Bookmark, BookmarkBorder, ContentCopy, Undo } from "@mui/icons-material"; import { Fab, IconButton, useTheme } from "@mui/material"; +import { adSpaceSize, UnitType } from "@src/components/AdSpace"; import InputDialog from "@src/components/InputDialog"; +import { playwireUnitMobileBottomRail } from "@src/constants"; import useIsMobile from "@src/hooks/is-mobile"; import { buildModelView, lastSelectedBuildModelView } from "@src/state/build"; +import { eventsAtom, playwireIsUnitInitialized } from "@src/state/events"; import { addFavorite, favoritesAtom, @@ -12,6 +15,7 @@ import { } from "@src/state/favorites"; import { buildIdRegex } from "@src/utils/build-id"; import { defaultBuildName } from "@src/utils/default-build-name"; +import { adsEnabled } from "@src/utils/env-tools"; import { useAtomValue, useSetAtom } from "jotai"; import { useSnackbar } from "notistack"; import React, { useState } from "react"; @@ -39,6 +43,8 @@ const BuildMenu: React.FC = () => { const isFavorite = isBuildInFavorites(favorites, buildId); const isCopyToClipboardEnabled = navigator.clipboard !== undefined; + const events = useAtomValue(eventsAtom); + if (buildIdRegex.exec(location.pathname) === null) { return null; } @@ -93,7 +99,14 @@ const BuildMenu: React.FC = () => { color="primary" onClick={handleCopyToClipboardClicked} sx={{ - bottom: theme.spacing(2), + bottom: + (adsEnabled || DB_DISPLAY_AD_PLACEHOLDERS) && + playwireIsUnitInitialized(events, playwireUnitMobileBottomRail) + ? `${ + adSpaceSize[UnitType.BottomRail].height + + parseInt(theme.spacing(3).slice(0, -2)) + }px` + : theme.spacing(2), // TODO: edit this position: "fixed", right: theme.spacing(3), }} diff --git a/src/components/CenterBox.tsx b/src/components/CenterBox.tsx new file mode 100644 index 000000000..c81c001a5 --- /dev/null +++ b/src/components/CenterBox.tsx @@ -0,0 +1,22 @@ +import { Box } from "@mui/material"; +import React from "react"; + +interface CenterBoxProps { + flexGrow?: number; + children: React.ReactElement; +} + +const CenterBox: React.FC = ({ flexGrow, children }) => ( + + {children} + +); + +export default React.memo(CenterBox); diff --git a/src/components/ConstraintBox.tsx b/src/components/ConstraintBox.tsx new file mode 100644 index 000000000..404f249cc --- /dev/null +++ b/src/components/ConstraintBox.tsx @@ -0,0 +1,54 @@ +import useWindowSize from "@src/hooks/window-size"; +import React from "react"; + +export interface Constraint { + matchesConstraint(windowWidth: number, windowHeight: number): boolean; +} + +export class WithinBoundsConstraint implements Constraint { + maxWidth: number; + maxHeight: number; + + constructor(maxWidth: number, maxHeight: number) { + this.maxWidth = maxWidth; + this.maxHeight = maxHeight; + } + + matchesConstraint(windowWidth: number, windowHeight: number): boolean { + return windowWidth <= this.maxWidth && windowHeight <= this.maxHeight; + } +} + +export class OutsideBoundsConstraint implements Constraint { + minWidth: number; + minHeight: number; + + constructor(minWidth: number, minHeight: number) { + this.minWidth = minWidth; + this.minHeight = minHeight; + } + + matchesConstraint(windowWidth: number, windowHeight: number): boolean { + return windowWidth >= this.minWidth && windowHeight >= this.minHeight; + } +} + +interface ConstraintBoxProps { + constraints: Constraint[]; + children: React.ReactElement; + renderOnFail?: React.ReactElement | null; +} + +const ConstraintBox: React.FC = ({ constraints, children, renderOnFail }) => { + const { width, height } = useWindowSize(); + + for (const constraint of constraints) { + if (!constraint.matchesConstraint(width, height)) { + return renderOnFail; + } + } + + return children; +}; + +export default React.memo(ConstraintBox); diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index aeb89b5ea..f5d063fbf 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -9,6 +9,7 @@ import { ManageSearch, Menu, Settings, + Shield, Stars, } from "@mui/icons-material"; import { @@ -30,10 +31,14 @@ import { Typography, useTheme, } from "@mui/material"; +import { UnitType } from "@src/components/AdSpace"; +import AdSpaceFloating from "@src/components/AdSpaceFloating"; import BuildMenu from "@src/components/BuildMenu"; import LinkBox from "@src/components/LinkBox"; +import SomethingWentWrong from "@src/components/SometingWentWrong"; +import Spacer from "@src/components/Spacer"; import { drawerWidth } from "@src/components/theme"; -import { crowdinLink, discordServerUrl, githubUrl, xTwitterUrl } from "@src/constants"; +import { crowdinLink, discordServerUrl, githubUrl, playwireUnitMobileBottomRail, xTwitterUrl } from "@src/constants"; import dauntlessBuilderData from "@src/data/Data"; import useDevMode from "@src/hooks/dev-mode"; import useIsMobile from "@src/hooks/is-mobile"; @@ -50,7 +55,6 @@ import { NavLink } from "react-router-dom"; import { AppBar } from "./AppBar"; import { DrawerHeader } from "./Drawer"; -import SomethingWentWrong from "./SomethingWentWrong"; interface LayoutProps { children: ReactNode; @@ -76,6 +80,7 @@ const Layout: React.FC = ({ children }) => { { icon: , link: "/b/finder", text: t("drawer.build-finder") }, { icon: , link: "/b/meta", text: t("drawer.meta-builds") }, { icon: , link: "/about", text: t("drawer.about") }, + { icon: , link: "/privacy", text: t("drawer.privacy") }, { icon: , link: "/settings", text: t("drawer.settings") }, ]; @@ -132,7 +137,7 @@ const Layout: React.FC = ({ children }) => { /> ) : null} - {/* Spacer */} + @@ -178,12 +183,13 @@ const Layout: React.FC = ({ children }) => { ))} + + + = ({ children }) => { + @@ -255,6 +262,15 @@ const Layout: React.FC = ({ children }) => { {children} + + ); }; diff --git a/src/components/SometingWentWrong.tsx b/src/components/SometingWentWrong.tsx new file mode 100644 index 000000000..7389bd79a --- /dev/null +++ b/src/components/SometingWentWrong.tsx @@ -0,0 +1,29 @@ +import { Download } from "@mui/icons-material"; +import { Alert, Box, Button, Card } from "@mui/material"; +import { downloadJsonObject } from "@src/utils/download-json"; +import log, { Logger } from "@src/utils/logger"; +import React from "react"; +import { FallbackProps } from "react-error-boundary/dist/declarations/src/types"; +import { useTranslation } from "react-i18next"; + +const SomethingWentWrong = ({ error }: FallbackProps) => { + const { t } = useTranslation(); + + log.error(error.message); + + return ( + + {t("misc.something-went-wrong")} + {error.message} + + + ); +}; + +export default React.memo(SomethingWentWrong); diff --git a/src/components/Spacer.tsx b/src/components/Spacer.tsx new file mode 100644 index 000000000..4e492fb7b --- /dev/null +++ b/src/components/Spacer.tsx @@ -0,0 +1,6 @@ +import { Box } from "@mui/material"; +import React from "react"; + +const Spacer = () => ; + +export default React.memo(Spacer); diff --git a/src/components/TrackingRampSetup.tsx b/src/components/TrackingRampSetup.tsx new file mode 100644 index 000000000..416a7d496 --- /dev/null +++ b/src/components/TrackingRampSetup.tsx @@ -0,0 +1,86 @@ +import { eventsAtom, playwireClearInitializedUnits, playwireSetupHasFinished } from "@src/state/events"; +import { adsEnabled } from "@src/utils/env-tools"; +import log from "@src/utils/logger"; +import { useSetAtom } from "jotai"; +import React, { useEffect } from "react"; +import ReactGA from "react-ga4"; +import { useLocation } from "react-router-dom"; + +const TrackingRampSetup = () => { + const setEvents = useSetAtom(eventsAtom); + const location = useLocation(); + + useEffect(() => { + const ga4Enabled = navigator.doNotTrack !== "1" && DB_GA4_MEASUREMENT_ID !== null; + + if (ga4Enabled) { + ReactGA.initialize(DB_GA4_MEASUREMENT_ID, { + testMode: DB_DEVMODE, + }); + log.debug("enabled GA4"); + } + + if (adsEnabled && !DB_DISPLAY_AD_PLACEHOLDERS) { + if (window.ramp) { + return; + } + + window.ramp = window.ramp ?? {}; + window.ramp.que = window.ramp.que ?? []; + window.ramp.passiveMode = true; + + if (ga4Enabled) { + const gtagFunc = (...args: unknown[]) => window.dataLayer.push(args); + + window._pwGA4PageviewId = Date.now().toString(); + window.dataLayer = window.dataLayer || []; + window.gtag = window.gtag || gtagFunc; + window.gtag("js", new Date()); + window.gtag("config", DB_GA4_MEASUREMENT_ID, { send_page_view: false }); + window.gtag("event", "ramp_js", { + pageview_id: window._pwGA4PageviewId, + send_to: DB_GA4_MEASUREMENT_ID, + }); + } + + window.ramp.que.push(() => { + log.debug("playwire has been setup"); + setEvents(playwireSetupHasFinished()); + }); + + const rampScript = document.createElement("script"); + rampScript.src = `https://cdn.intergient.com/${DB_PW_PUBLISHER_ID}/${DB_PW_WEBSITE_ID}/ramp.js`; + rampScript.async = true; + document.body.appendChild(rampScript); + } + + if (adsEnabled || DB_DISPLAY_AD_PLACEHOLDERS) { + let timer: NodeJS.Timeout | null = null; + + window.addEventListener("resize", () => { + if (timer) { + clearTimeout(timer); + } + timer = setTimeout(() => { + if (!window.ramp) { + return; + } + + log.debug("resized window: destroy units..."); + window.ramp.destroyUnits("all"); + setEvents(playwireClearInitializedUnits()); + }, 100); + }); + } + }, [setEvents]); + + useEffect(() => { + if (adsEnabled && !DB_DISPLAY_AD_PLACEHOLDERS) { + window.ramp.processPage(location.pathname); + } + }, [location]); + + return null; +}; + +export default React.memo(TrackingRampSetup); diff --git a/src/constants.ts b/src/constants.ts index 922569922..36c05bfea 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,13 @@ export const issuesUrl = "https://github.com/atomicptr/dauntless-builder/issues"; -export const xTwitterUrl = "https://x.com/dauntlessbuildr"; +export const xTwitterUrl = "https://x.com/atomicptr"; export const githubUrl = "https://github.com/atomicptr/dauntless-builder"; export const discordServerUrl = "https://discord.gg/hkMvhsfPjH"; export const crowdinLink = "https://crowdin.com/project/dauntless-builder"; export const licenseUrl = "https://github.com/atomicptr/dauntless-builder/blob/master/LICENSE"; export const assetsCdnPrefix = "https://cdn.jsdelivr.net/gh/atomicptr/dauntless-builder"; + +export const playwireUnitRightSide = "SkyscraperRightSide"; +export const playwireUnitMobileBottomRail = "MobileBottomRail"; +export const playwireUnitLeftSide = "LeftSideBar"; +export const playwireUnitUnderPerkList = "UnderPerkList"; +export const playwireUnitLeftSideSmall = "LeftSideBarSmaller"; diff --git a/src/hooks/window-size.ts b/src/hooks/window-size.ts new file mode 100644 index 000000000..e4a51c7c5 --- /dev/null +++ b/src/hooks/window-size.ts @@ -0,0 +1,15 @@ +import { useState } from "react"; + +const useWindowSize = () => { + const [width, setWidth] = useState(window.innerWidth); + const [height, setHeight] = useState(window.innerHeight); + + window.addEventListener("resize", () => { + setWidth(window.innerWidth); + setHeight(window.innerHeight); + }); + + return { height, width }; +}; + +export default useWindowSize; diff --git a/src/json/i18n/en/en.json b/src/json/i18n/en/en.json index 9680622cc..5dde774a9 100644 --- a/src/json/i18n/en/en.json +++ b/src/json/i18n/en/en.json @@ -36,14 +36,17 @@ "meta-builds": "$t(pages.metabuilds.title)", "my-builds": "$t(pages.favorites.title)", "new-build": "New Build", + "privacy": "$t(pages.privacy.title)", "settings": "$t(pages.settings.title)", "trials": "$t(pages.trials.title)" }, "feature-disabled-browser": "This feature is currently disabled for this web browser.", "misc": { + "ad": "Ad", "dauntless-version": "Dauntless v{{version}}", "discord-server": "Dauntless Builder Discord Server", "github-repository": "Github Repository", + "only-english": "This part is only available in English.", "patch-url": "https://playdauntless.com/patch-notes/{{version}}", "something-went-wrong": "Something went wrong! Please send the error log below to me in our Discord and try to reload the website. Thank you very much!", "x-twitter": "Dauntless Builder on X.com" @@ -59,8 +62,6 @@ "contributors": "Contributors", "dependencies": "Dependencies ({{number}})", "main-text": "Dauntless Builder is free and open source software licensed under the terms of the AGPLv3 license.", - "privacy": "Privacy", - "privacy-text": "We are not collecting any data.", "source-code": "Source Code", "title": "About", "translators": "Translators" @@ -233,6 +234,10 @@ "newbuild": { "title": "Create a new build" }, + "privacy": { + "title": "Privacy", + "title-alt": "Privacy Policy for Dauntless Builder" + }, "settings": { "community-language": "This language is not officially supported by Dauntless, but members of our community decided to add a translation for Dauntless Builder regardless.", "data": "Application Data", diff --git a/src/pages/about/About.tsx b/src/pages/about/About.tsx index 2106b0ceb..225bc9777 100644 --- a/src/pages/about/About.tsx +++ b/src/pages/about/About.tsx @@ -168,15 +168,6 @@ const About: React.FC = () => { - - {t("pages.about.privacy")} - - - {t("pages.about.privacy-text")} - { + const { t } = useTranslation(); + const theme = useTheme(); + + const PrivacyPoliczWrapper = styled("div")` + a { + color: ${theme.palette.primary.main}; + text-decoration: none; + } + `; + + return ( + + + + } + > + {t("misc.only-english")} + + + +
+ + + ); +}; + +export default Privacy; diff --git a/src/pages/about/privacy.html b/src/pages/about/privacy.html new file mode 100644 index 000000000..21788a31a --- /dev/null +++ b/src/pages/about/privacy.html @@ -0,0 +1,211 @@ +

+ At Dauntless Builder, accessible from https://www.dauntless-builder.com, one of our main priorities is the privacy + of our visitors. This Privacy Policy document contains types of information that is collected and recorded by + Dauntless Builder and how we use it. +

+ +

+ If you have additional questions or require more information about our Privacy Policy, do not hesitate to contact + us. +

+ +

+ This Privacy Policy applies only to our online activities and is valid for visitors to our website with regards to + the information that they shared and/or collect in Dauntless Builder. This policy is not applicable to any + information collected offline or via channels other than this website. +

+ +

Consent

+ +

By using our website, you hereby consent to our Privacy Policy and agree to its terms.

+ +

Information we collect

+ +

+ The personal information that you are asked to provide, and the reasons why you are asked to provide it, will be + made clear to you at the point we ask you to provide your personal information. +

+

+ If you contact us directly, we may receive additional information about you such as your name, email address, phone + number, the contents of the message and/or attachments you may send us, and any other information you may choose to + provide. +

+

+ When you register for an Account, we may ask for your contact information, including items such as name, company + name, address, email address, and telephone number. +

+ +

How we use your information

+ +

We use the information we collect in various ways, including to:

+ +
    +
  • Provide, operate, and maintain our website
  • +
  • Improve, personalize, and expand our website
  • +
  • Understand and analyze how you use our website
  • +
  • Develop new products, services, features, and functionality
  • +
  • + Communicate with you, either directly or through one of our partners, including for customer service, to provide + you with updates and other information relating to the website, and for marketing and promotional purposes +
  • +
  • Send you emails
  • +
  • Find and prevent fraud
  • +
+ +

Log Files

+ +

+ Dauntless Builder follows a standard procedure of using log files. These files log visitors when they visit + websites. All hosting companies do this and a part of hosting services' analytics. The information collected by log + files include internet protocol (IP) addresses, browser type, Internet Service Provider (ISP), date and time stamp, + referring/exit pages, and possibly the number of clicks. These are not linked to any information that is personally + identifiable. The purpose of the information is for analyzing trends, administering the site, tracking users' + movement on the website, and gathering demographic information. +

+ +

Cookies and Web Beacons

+ +

+ Like any other website, Dauntless Builder uses "cookies". These cookies are used to store information including + visitors' preferences, and the pages on the website that the visitor accessed or visited. The information is used to + optimize the users' experience by customizing our web page content based on visitors' browser type and/or other + information. +

+ +

Our Advertising Partners

+ +

+ Some of advertisers on our site may use cookies and web beacons. Our advertising partners are listed below. Each of + our advertising partners has their own Privacy Policy for their policies on user data. For easier access, we + hyperlinked to their Privacy Policies below. +

+ +

Google

+ +https://policies.google.com/technologies/ads + +

Playwire LLC

+ +

+ All or partial advertising on this Website or App is managed by Playwire LLC. If Playwire publisher advertising + services are used, Playwire LLC may collect and use certain aggregated and anonymized data for advertising purposes. + To learn more about the types of data collected, how data is used and your choices as a user, please visit + https://www.playwire.com/privacy-policy. +

+

+ For EU Users only: If you are located in countries that are part of the European Economic Area, in + the United Kingdom or Switzerland, and publisher advertising services are being provided by Playwire LLC, you were + presented with messaging from our Consent Management Platform (CMP) around your privacy choices as a user in regards + to digital advertising, applicable vendors, cookie usage and more. If you’d like to revisit the choices you have + made previously on this Website or App, please + click here. +

+ +

Advertising Partners Privacy Policies

+ +

You may consult this list to find the Privacy Policy for each of the advertising partners of Dauntless Builder.

+ +

+ Third-party ad servers or ad networks uses technologies like cookies, JavaScript, or Web Beacons that are used in + their respective advertisements and links that appear on Dauntless Builder, which are sent directly to users' + browser. They automatically receive your IP address when this occurs. These technologies are used to measure the + effectiveness of their advertising campaigns and/or to personalize the advertising content that you see on websites + that you visit. +

+ +

+ Note that Dauntless Builder has no access to or control over these cookies that are used by third-party advertisers. +

+ +

Third Party Privacy Policies

+ +

+ Dauntless Builder's Privacy Policy does not apply to other advertisers or websites. Thus, we are advising you to + consult the respective Privacy Policies of these third-party ad servers for more detailed information. It may + include their practices and instructions about how to opt-out of certain options. +

+ +

+ You can choose to disable cookies through your individual browser options. To know more detailed information about + cookie management with specific web browsers, it can be found at the browsers' respective websites. +

+ +

CCPA Privacy Rights (Do Not Sell My Personal Information)

+ +

Under the CCPA, among other rights, California consumers have the right to:

+

+ Request that a business that collects a consumer's personal data disclose the categories and specific pieces of + personal data that a business has collected about consumers. +

+

Request that a business delete any personal data about the consumer that a business has collected.

+

Request that a business that sells a consumer's personal data, not sell the consumer's personal data.

+

+ If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, + please contact us. +

+ +

GDPR Data Protection Rights

+ +

+ We would like to make sure you are fully aware of all of your data protection rights. Every user is entitled to the + following: +

+

+ The right to access – You have the right to request copies of your personal data. We may charge you a small fee for + this service. +

+

+ The right to rectification – You have the right to request that we correct any information you believe is + inaccurate. You also have the right to request that we complete the information you believe is incomplete. +

+

The right to erasure – You have the right to request that we erase your personal data, under certain conditions.

+

+ The right to restrict processing – You have the right to request that we restrict the processing of your personal + data, under certain conditions. +

+

+ The right to object to processing – You have the right to object to our processing of your personal data, under + certain conditions. +

+

+ The right to data portability – You have the right to request that we transfer the data that we have collected to + another organization, or directly to you, under certain conditions. +

+

+ If you make a request, we have one month to respond to you. If you would like to exercise any of these rights, + please contact us. +

+ +

Children's Information

+ +

+ Another part of our priority is adding protection for children while using the internet. We encourage parents and + guardians to observe, participate in, and/or monitor and guide their online activity. +

+ +

+ Dauntless Builder does not knowingly collect any Personal Identifiable Information from children under the age of + 13. If you think that your child provided this kind of information on our website, we strongly encourage you to + contact us immediately and we will do our best efforts to promptly remove such information from our records. +

+ +

Changes to This Privacy Policy

+ +

+ We may update our Privacy Policy from time to time. Thus, we advise you to review this page periodically for any + changes. We will notify you of any changes by posting the new Privacy Policy on this page. These changes are + effective immediately, after they are posted on this page. +

+ +

+ Our Privacy Policy was created with the help of the + Privacy Policy Generator. +

+ +

Contact Us

+ +

If you have any questions or suggestions about our Privacy Policy, do not hesitate to contact us.

diff --git a/src/pages/build/Build.tsx b/src/pages/build/Build.tsx index 01e5bf6ba..1aba0ebac 100644 --- a/src/pages/build/Build.tsx +++ b/src/pages/build/Build.tsx @@ -1,10 +1,13 @@ import { ManageSearch } from "@mui/icons-material"; import { Box, Button, Grid, ListSubheader, Typography } from "@mui/material"; +import AdSpace, { UnitType } from "@src/components/AdSpace"; import BondWeaponPicker from "@src/components/BondWeaponPicker"; import BuildWarning from "@src/components/BuildWarning"; import CellPicker from "@src/components/CellPicker"; import CellSelectDialog from "@src/components/CellSelectDialog"; import CellSlotFilter from "@src/components/CellSlotFilter"; +import CenterBox from "@src/components/CenterBox"; +import ConstraintBox from "@src/components/ConstraintBox"; import ElementalTypeFilter from "@src/components/ElementalTypeFilter"; import GenericItemSelectDialog, { GenericItem } from "@src/components/GenericItemSelectDialog"; import ItemPicker, { ItemPickerItem } from "@src/components/ItemPicker"; @@ -25,6 +28,7 @@ import PerkListMobile from "@src/components/PerkListMobile"; import TagIcons from "@src/components/TagIcons"; import UniqueEffectCard from "@src/components/UniqueEffectCard"; import WeaponTypeFilter from "@src/components/WeaponTypeFilter"; +import { playwireUnitUnderPerkList } from "@src/constants"; import { Armour, ArmourType } from "@src/data/Armour"; import { BuildModel, findPartSlotName } from "@src/data/BuildModel"; import { CellType } from "@src/data/Cell"; @@ -495,6 +499,15 @@ const Build: React.FC = () => { > {isMobile ? : } + + + + + +