diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx
deleted file mode 100644
index 3d759f8b87..0000000000
--- a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings.tsx
+++ /dev/null
@@ -1,285 +0,0 @@
-import Box from "@mui/material/Box";
-import Button from "@mui/material/Button";
-import Link from "@mui/material/Link";
-import { styled } from "@mui/material/styles";
-import Typography from "@mui/material/Typography";
-import { useFormik } from "formik";
-import { hasFeatureFlag } from "lib/featureFlags";
-import React from "react";
-import ColorPicker from "ui/editor/ColorPicker";
-import EditorRow from "ui/editor/EditorRow";
-import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder";
-import InputDescription from "ui/editor/InputDescription";
-import InputGroup from "ui/editor/InputGroup";
-import InputLegend from "ui/editor/InputLegend";
-import InputRow from "ui/shared/InputRow";
-import InputRowItem from "ui/shared/InputRowItem";
-import InputRowLabel from "ui/shared/InputRowLabel";
-import PublicFileUploadButton from "ui/shared/PublicFileUploadButton";
-
-const DesignPreview = styled(Box)(({ theme }) => ({
- border: `2px solid ${theme.palette.border.input}`,
- padding: theme.spacing(2),
- boxShadow: "4px 4px 0px rgba(150, 150, 150, 0.5)",
-}));
-
-const exampleColor = "#007078";
-
-const DesignSettings: React.FC = () => {
- const formik = useFormik<{
- themeColor: string;
- buttonColor: string;
- linkColor: string;
- }>({
- initialValues: {
- themeColor: exampleColor,
- buttonColor: exampleColor,
- linkColor: exampleColor,
- },
- onSubmit: () => {},
- validate: () => {},
- });
-
- const isUsingFeatureFlag = () => hasFeatureFlag("SHOW_TEAM_SETTINGS");
-
- return (
- <>
-
-
- Design
-
-
- How your service appears to public users
-
-
- {!isUsingFeatureFlag() ? (
-
- {" "}
-
- ) : (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
- >
- )}
- >
- );
-};
-
-export default DesignSettings;
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx
new file mode 100644
index 0000000000..ddeb8d32c9
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ButtonForm.tsx
@@ -0,0 +1,61 @@
+import Button from "@mui/material/Button";
+import Link from "@mui/material/Link";
+import { useFormik } from "formik";
+import React from "react";
+import ColorPicker from "ui/editor/ColorPicker";
+import InputDescription from "ui/editor/InputDescription";
+import InputRow from "ui/shared/InputRow";
+import InputRowItem from "ui/shared/InputRowItem";
+
+import { DesignPreview, EXAMPLE_COLOUR, SettingsForm } from ".";
+
+export const ButtonForm: React.FC = () => {
+ const formik = useFormik<{
+ buttonColor: string;
+ }>({
+ initialValues: {
+ buttonColor: EXAMPLE_COLOUR,
+ },
+ onSubmit: () => {},
+ validate: () => {},
+ });
+
+ return (
+
+
+ The button background colour should be either a dark or light
+ colour. The text will be programmatically selected to contrast with
+ the selected colour (being either black or white).
+
+
+
+ See our guide for setting button colours
+
+
+ >
+ }
+ input={
+
+
+ formik.setFieldValue("buttonColor", color)}
+ label="Button colour"
+ />
+
+
+ }
+ preview={
+
+
+
+ }
+ />
+ );
+};
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx
new file mode 100644
index 0000000000..729f011d21
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/FaviconForm.tsx
@@ -0,0 +1,57 @@
+import Link from "@mui/material/Link";
+import Typography from "@mui/material/Typography";
+import { useFormik } from "formik";
+import React from "react";
+import InputDescription from "ui/editor/InputDescription";
+import InputRow from "ui/shared/InputRow";
+import InputRowItem from "ui/shared/InputRowItem";
+import InputRowLabel from "ui/shared/InputRowLabel";
+import PublicFileUploadButton from "ui/shared/PublicFileUploadButton";
+
+import { EXAMPLE_COLOUR, SettingsForm } from ".";
+
+export const FaviconForm: React.FC = () => {
+ const formik = useFormik<{
+ textLinkColor: string;
+ }>({
+ initialValues: {
+ textLinkColor: EXAMPLE_COLOUR,
+ },
+ onSubmit: () => {},
+ validate: () => {},
+ });
+
+ return (
+
+
+ Set the favicon to be used in the browser tab. The favicon should be
+ 32x32px and in .ico or .png format.
+
+
+ See our guide for favicons
+
+ >
+ }
+ input={
+
+ Favicon:
+
+
+
+
+ .ico or .png
+
+
+ }
+ />
+ );
+};
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx
new file mode 100644
index 0000000000..f89d7c0223
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/TextLinkForm.tsx
@@ -0,0 +1,57 @@
+import Link from "@mui/material/Link";
+import { useFormik } from "formik";
+import React from "react";
+import ColorPicker from "ui/editor/ColorPicker";
+import InputDescription from "ui/editor/InputDescription";
+import InputRow from "ui/shared/InputRow";
+import InputRowItem from "ui/shared/InputRowItem";
+
+import { DesignPreview, EXAMPLE_COLOUR, SettingsForm } from ".";
+
+export const TextLinkForm: React.FC = () => {
+ const formik = useFormik<{
+ linkColor: string;
+ }>({
+ initialValues: {
+ linkColor: EXAMPLE_COLOUR,
+ },
+ onSubmit: () => {},
+ validate: () => {},
+ });
+
+ return (
+
+
+ The text link colour should be a dark colour that contrasts with
+ white ("#ffffff").
+
+
+
+ See our guide for setting text link colours
+
+
+ >
+ }
+ input={
+
+
+ formik.setFieldValue("linkColor", color)}
+ label="Text link colour"
+ />
+
+
+ }
+ preview={
+
+ Example text link
+
+ }
+ />
+ );
+};
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx
new file mode 100644
index 0000000000..8e1f2e8286
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/ThemeAndLogoForm.tsx
@@ -0,0 +1,127 @@
+import Link from "@mui/material/Link";
+import { getContrastRatio, useTheme } from "@mui/material/styles";
+import Typography from "@mui/material/Typography";
+import { Team, TeamTheme } from "@opensystemslab/planx-core/types";
+import { useFormik } from "formik";
+import { useStore } from "pages/FlowEditor/lib/store";
+import React, { useEffect, useState } from "react";
+import ColorPicker from "ui/editor/ColorPicker";
+import InputDescription from "ui/editor/InputDescription";
+import InputRow from "ui/shared/InputRow";
+import InputRowItem from "ui/shared/InputRowItem";
+import InputRowLabel from "ui/shared/InputRowLabel";
+import PublicFileUploadButton from "ui/shared/PublicFileUploadButton";
+
+import { DesignPreview, SettingsForm } from ".";
+
+type FormValues = Pick;
+
+export const ThemeAndLogoForm: React.FC<{
+ team: Team;
+ onSuccess: () => void;
+}> = ({ team, onSuccess }) => {
+ const theme = useTheme();
+
+ useEffect(() => {
+ setInitialValues({
+ primaryColour: team.theme?.primaryColour,
+ logo: team.theme?.logo,
+ });
+ }, [team]);
+
+ const [initialValues, setInitialValues] = useState({
+ primaryColour: "",
+ logo: "",
+ });
+
+ const formik = useFormik({
+ initialValues,
+ onSubmit: async (values, { resetForm }) => {
+ const isSuccess = await useStore.getState().updateTeamTheme(values);
+ if (isSuccess) {
+ onSuccess();
+ // Reset "dirty" status to disable Save & Reset buttons
+ resetForm({ values });
+ }
+ },
+ validateOnBlur: false,
+ validateOnChange: false,
+ enableReinitialize: true,
+ validate: ({ primaryColour }) => {
+ const isContrastThresholdMet =
+ getContrastRatio("#FFF", primaryColour) >
+ theme.palette.contrastThreshold;
+
+ if (!isContrastThresholdMet) {
+ return {
+ primaryColour:
+ "Theme colour does not meet accessibility contrast requirements (3:1)",
+ };
+ }
+ },
+ });
+
+ return (
+
+
+ The theme colour and logo, are used in the header of the service.
+ The theme colour should be a dark colour that contrasts with white
+ ("#ffffff"). The logo should contrast with a dark background colour
+ (your theme colour) and have a transparent background.
+
+
+
+ See our guide for setting theme colours and logos
+
+
+ >
+ }
+ input={
+ <>
+
+
+
+ formik.setFieldValue("primaryColour", color)
+ }
+ label="Theme colour"
+ />
+
+
+
+ Logo:
+
+ formik.setFieldValue("logo", newUrl)}
+ />
+
+
+ .png or .svg
+
+
+ >
+ }
+ preview={
+
+ {formik.values.logo ? (
+
+ ) : (
+
+ {team?.name}
+
+ )}
+
+ }
+ />
+ );
+};
diff --git a/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx
new file mode 100644
index 0000000000..4bc13a3951
--- /dev/null
+++ b/editor.planx.uk/src/pages/FlowEditor/components/Settings/DesignSettings/index.tsx
@@ -0,0 +1,158 @@
+import Alert from "@mui/material/Alert";
+import Box from "@mui/material/Box";
+import Button from "@mui/material/Button";
+import Snackbar from "@mui/material/Snackbar";
+import { styled } from "@mui/material/styles";
+import Typography from "@mui/material/Typography";
+import { Team } from "@opensystemslab/planx-core/types";
+import { FormikProps, getIn } from "formik";
+import { hasFeatureFlag } from "lib/featureFlags";
+import { useStore } from "pages/FlowEditor/lib/store";
+import React, { useEffect, useState } from "react";
+import EditorRow from "ui/editor/EditorRow";
+import { FeaturePlaceholder } from "ui/editor/FeaturePlaceholder";
+import InputGroup from "ui/editor/InputGroup";
+import InputLegend from "ui/editor/InputLegend";
+import ErrorWrapper from "ui/shared/ErrorWrapper";
+
+import { ButtonForm } from "./ButtonForm";
+import { FaviconForm } from "./FaviconForm";
+import { TextLinkForm } from "./TextLinkForm";
+import { ThemeAndLogoForm } from "./ThemeAndLogoForm";
+
+export const DesignPreview = styled(Box)(({ theme }) => ({
+ border: `2px solid ${theme.palette.border.input}`,
+ padding: theme.spacing(2),
+ boxShadow: "4px 4px 0px rgba(150, 150, 150, 0.5)",
+}));
+
+export const EXAMPLE_COLOUR = "#007078";
+
+type SettingsFormProps = {
+ legend: string;
+ description: React.ReactElement;
+ input: React.ReactElement;
+ formik: FormikProps;
+ preview?: React.ReactElement;
+};
+
+const useTeam = () => {
+ const [team, setTeam] = useState({} as Team);
+
+ useEffect(() => {
+ const fetchTeam = async () => {
+ try {
+ const fetchedTeam = await useStore.getState().fetchCurrentTeam();
+ if (!fetchedTeam) throw Error("Unable to find team");
+ setTeam(fetchedTeam);
+ } catch (error) {
+ console.error("Error fetching team:", error);
+ }
+ };
+
+ fetchTeam();
+ }, []);
+
+ return team;
+};
+
+export const SettingsForm: React.FC = ({
+ formik,
+ legend,
+ description,
+ input,
+ preview,
+}) => {
+ return (
+
+
+
+ );
+};
+
+const DesignSettings: React.FC = () => {
+ const isUsingFeatureFlag = hasFeatureFlag("SHOW_TEAM_SETTINGS");
+ const team = useTeam();
+ const [open, setOpen] = useState(false);
+
+ const handleClose = (
+ _event?: React.SyntheticEvent | Event,
+ reason?: string,
+ ) => {
+ if (reason === "clickaway") {
+ return;
+ }
+
+ setOpen(false);
+ };
+
+ return (
+ <>
+
+
+ Design
+
+
+ How your service appears to public users
+
+
+ {!isUsingFeatureFlag ? (
+
+ {" "}
+
+ ) : (
+ <>
+ setOpen(true)} />
+
+
+
+
+
+ Theme updated successfully
+
+
+ >
+ )}
+ >
+ );
+};
+
+export default DesignSettings;
diff --git a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts
index 7506f3c70b..c7b1e11c2d 100644
--- a/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts
+++ b/editor.planx.uk/src/pages/FlowEditor/lib/store/team.ts
@@ -24,6 +24,7 @@ export interface TeamStore {
initTeamStore: (slug: string) => Promise;
clearTeamStore: () => void;
fetchCurrentTeam: () => Promise;
+ updateTeamTheme: (theme: Partial) => Promise;
}
export const teamStore: StateCreator<
@@ -115,4 +116,10 @@ export const teamStore: StateCreator<
const team = await $client.team.getBySlug(teamSlug);
return team;
},
+
+ updateTeamTheme: async (theme: Partial) => {
+ const { teamId, $client } = get();
+ const isSuccess = await $client.team.updateTheme(teamId, theme);
+ return isSuccess;
+ },
});