diff --git a/frontend/src/assets/template/make_a_wish_tour_social_a2/background.png b/frontend/src/assets/template/make_a_wish_tour_social_a2/background.png
new file mode 100644
index 0000000000000000000000000000000000000000..e5e6e89f3134d464bb642e003757217d647258ca
Binary files /dev/null and b/frontend/src/assets/template/make_a_wish_tour_social_a2/background.png differ
diff --git a/frontend/src/templates.js b/frontend/src/templates.js
index c210eca30a13c80e01ebee7e76fbe16b4e86e992..592222251daacd2f35aecf89946706b3c3aedfc7 100644
--- a/frontend/src/templates.js
+++ b/frontend/src/templates.js
@@ -67,6 +67,15 @@ const TEMPLATES = {
       title: "Máte přání tour banner na soc. sítě",
     },
   },
+  make_a_wish_tour_social_a2: {
+    name: "A2 - Máte přání tour banner na soc. sítě",
+    image: makeawishTourSocialImage,
+    path: "/make-a-wish-tour-social-a2",
+    component: () => import("./views/make_a_wish_tour_social_A2/MakeAWishTourSocialA2.vue"),
+    meta: {
+      title: "A2 - Máte přání tour banner na soc. sítě",
+    },
+  },
   back_in_full_force_banner: {
     name: "Zpátky v plné síle banner",
     image: backInFullForcePhotoBannerImage,
@@ -85,6 +94,15 @@ const TEMPLATES = {
       title: "Zpátky v plné síle tour banner na soc. sítě",
     },
   },
+  back_in_full_force_tour_social_A2: {
+    name: "A2 - Zpátky v plné síle tour banner na soc. sítě",
+    image: backInFullForceTourSocialImage,
+    path: "/back-in-full-force-tour-social-a2",
+    component: () => import("./views/back_in_full_force_tour_social_A2/BackInFullForceTourSocialA2.vue"),
+    meta: {
+      title: "A2 - Zpátky v plné síle tour banner na soc. sítě",
+    },
+  },
   urgent_text_banner: {
     name: "Urgentní banner pouze s textem",
     image: urgentTextBannerImage,
diff --git a/frontend/src/views/back_in_full_force_tour_social_A2/BackInFullForceTourSocialA2.vue b/frontend/src/views/back_in_full_force_tour_social_A2/BackInFullForceTourSocialA2.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a9e4bccbe3bc09d8a61780cda2f1cea9732525dd
--- /dev/null
+++ b/frontend/src/views/back_in_full_force_tour_social_A2/BackInFullForceTourSocialA2.vue
@@ -0,0 +1,255 @@
+<script setup>
+import { watch, ref } from "vue";
+
+import COLORS from "../../colors";
+import PEOPLE from "../../people";
+import TEMPLATES from "../../templates";
+import DEFAULT_CONTRACTOR from "../../contractors";
+import {
+  loadFonts,
+  loadCanvasStorage,
+  setCanvasStorage,
+  updateAutoRedrawStorage,
+} from "../../utils";
+
+import Canvas from "../../components/canvas/Canvas.vue";
+import redraw from "./canvas";
+
+import Navbar from "../../components/Navbar.vue";
+import MainContainer from "../../components/MainContainer.vue";
+import ImageInput from "../../components/inputs/ImageInput.vue";
+import LongTextInput from "../../components/inputs/text/LongTextInput.vue";
+import ShortTextInput from "../../components/inputs/text/ShortTextInput.vue";
+import RangeInput from "../../components/inputs/RangeInput.vue";
+import InputSeparator from "../../components/inputs/InputSeparator.vue";
+import SelectInput from "../../components/inputs/SelectInput.vue";
+import MultipleColorPicker from "../../components/inputs/colors/MultipleColorPicker.vue";
+import ReloadButton from "../../components/reload/ReloadButton.vue";
+import AutoReloadCheckbox from "../../components/reload/AutoReloadCheckbox.vue";
+</script>
+
+<script>
+await loadFonts([
+  "12px Bebas Neue",
+  "12px Roboto Condensed",
+  "bold 12px Roboto Condensed",
+]);
+
+export default {
+  components: {
+    Canvas,
+    Navbar,
+    MainContainer,
+    ImageInput,
+    LongTextInput,
+    ShortTextInput,
+    RangeInput,
+    SelectInput,
+    InputSeparator,
+    MultipleColorPicker,
+  },
+  data() {
+    const predefinedColors = {
+      base: {
+        name: "Černé pozadí",
+        colors: {
+          background: COLORS.black,
+          highlight: COLORS.yellow1,
+          baseText: COLORS.white,
+          nameText: COLORS.yellow1,
+          highlightedText: COLORS.black,
+          contractedByText: COLORS.gray1,
+        },
+      },
+      white: {
+        name: "Bílé pozadí",
+        colors: {
+          background: COLORS.white,
+          highlight: COLORS.yellow1,
+          baseText: COLORS.black,
+          nameText: COLORS.black,
+          highlightedText: COLORS.black,
+          contractedByText: COLORS.gray1,
+        },
+      },
+    };
+
+    return {
+      mainImage: null,
+      mainText: null,
+
+      dateText: null,
+      timeText: null,
+      locationText: null,
+      attendeesText: null,
+
+      personName: null,
+      personPosition: null,
+      contractedBy: DEFAULT_CONTRACTOR,
+      colorLabels: {
+        background: "Pozadí",
+        highlight: "Zvýraznění",
+        baseText: "Město",
+        highlightedText: "Zvýrazněný text",
+      },
+      predefinedColors: predefinedColors,
+      colors: predefinedColors.base.colors,
+      autoRedraw: false,
+    };
+  },
+  async created() {
+    await loadCanvasStorage(this);
+  },
+  methods: {
+    async reloadCanvasProperties() {
+      const canvasProperties = {
+        mainImage: this.mainImage,
+        mainText: this.mainText,
+
+        dateText: this.dateText,
+        timeText: this.timeText,
+        locationText: this.locationText,
+        attendeesText: this.attendeesText,
+
+        personName: this.personName,
+        personPosition: this.personPosition,
+        contractedBy: this.contractedBy,
+        colors: this.colors,
+      };
+
+      if (canvasProperties.mainText) {
+        window.fileName = canvasProperties.mainText;
+      }
+
+      await this.$refs.canvas.redraw(canvasProperties);
+
+      delete canvasProperties.colors;
+      setCanvasStorage(canvasProperties);
+    },
+  },
+  mounted() {
+    this.$watch(
+      (vm) => [
+        vm.mainImage,
+        vm.mainText,
+        vm.dateText,
+        vm.timeText,
+        vm.locationText,
+        vm.attendeesText,
+        vm.personName,
+        vm.personPosition,
+        vm.contractedBy,
+        vm.colors,
+      ],
+      async (value) => {
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+
+    this.$watch(
+      (vm) => [vm.autoRedraw],
+      async (value) => {
+        updateAutoRedrawStorage(this.autoRedraw);
+
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+    );
+  },
+};
+</script>
+
+<template>
+  <header>
+    <Navbar :defaultTemplate="TEMPLATES.basic_photo_banner"></Navbar>
+  </header>
+  <main>
+    <MainContainer>
+      <template v-slot:left>
+        <Canvas
+          ref="canvas"
+          :redrawFunction="redraw"
+          width="2000"
+          height="2000"
+        />
+      </template>
+
+      <template v-slot:right>
+        <ReloadButton :parentRefs="$refs" @click="reloadCanvasProperties" />
+        <AutoReloadCheckbox v-model="autoRedraw" />
+        <ImageInput
+          name="Obrázek"
+          v-model="mainImage"
+          :important="true"
+          zIndex="10"
+        />
+        <LongTextInput
+          name="Město konání akce"
+          v-model="mainText"
+          :important="true"
+          :highlightable="true"
+          zIndex="9"
+        />
+
+        <ShortTextInput
+          name="Datum akce"
+          v-model="dateText"
+          :important="true"
+          zIndex="8"
+        />
+
+        <ShortTextInput
+          name="Čas akce"
+          v-model="timeText"
+          :important="true"
+          zIndex="7"
+        />
+
+        <LongTextInput
+          name="Účastníci akce"
+          v-model="attendeesText"
+          :important="true"
+          zIndex="6"
+        />
+
+        <LongTextInput
+          name="Přesné místo konání akce"
+          v-model="locationText"
+          :important="true"
+          zIndex="6"
+        />
+
+        <InputSeparator />
+
+        <MultipleColorPicker
+          name="Barvy"
+          v-model="colors"
+          :important="false"
+          :colorLabels="colorLabels"
+          :predefinedColors="predefinedColors"
+          :defaultPredefinedColors="predefinedColors.base"
+          zIndex="5"
+        ></MultipleColorPicker>
+
+        <ShortTextInput
+          name="Zadavatel a zpracovatel"
+          v-model="contractedBy"
+          :defaultValue="DEFAULT_CONTRACTOR"
+          :important="false"
+          zIndex="4"
+        />
+      </template>
+    </MainContainer>
+  </main>
+</template>
+
+<style>
+@import "vue-select/dist/vue-select.css";
+</style>
diff --git a/frontend/src/views/back_in_full_force_tour_social_A2/canvas.js b/frontend/src/views/back_in_full_force_tour_social_A2/canvas.js
new file mode 100644
index 0000000000000000000000000000000000000000..735fa7db5fd22b8c7067261d39e512a6437a60b8
--- /dev/null
+++ b/frontend/src/views/back_in_full_force_tour_social_A2/canvas.js
@@ -0,0 +1,465 @@
+import { fabric } from "fabric";
+import {
+  clearObjects,
+  sortObjects,
+  transformHighlightedText,
+  checkTextBoxHeight,
+} from "../../components/canvas/utils";
+import { PaddedHighlightingTextbox } from "../../components/canvas/textbox";
+import backgroundURL from "../../assets/template/back_in_full_force_tour_social/background.png";
+import backgroundURLInverted from "../../assets/template/make_a_wish_banner/background_inverted.png";
+import COLORS from "../../colors";
+
+let mainTextBox = null;
+
+let attendeesTextBox = null;
+let dateTextBox = null;
+let timeTextBox = null;
+let locationTextBox = null;
+
+let mainImage = null;
+let backgroundImage = null;
+let previousBackgroundImageColor = null;
+
+let contractedByTextbox = null;
+
+let mainImageSource = null;
+
+const removeDownEventListener = () => {
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerdown", canvasPointerDownEvent);
+};
+
+let upEventFunction = null;
+let canvasPointerDownEvent = null;
+let pointerDownEventAssigned = false;
+
+const redraw = async (canvas, options) => {
+  clearObjects(
+    [
+      mainTextBox,
+      contractedByTextbox,
+      attendeesTextBox,
+      dateTextBox,
+      timeTextBox,
+      locationTextBox
+    ],
+    canvas,
+  );
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerup", upEventFunction);
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerout", upEventFunction);
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointercancel", upEventFunction);
+
+  canvas.preserveObjectStacking = true;
+
+  const textMarginLeft = Math.ceil(canvas.width * 0.1);
+  const textMarginRight = Math.ceil(canvas.width * 0.078);
+
+  let mainTextMarginBottom = Math.ceil(canvas.height * 0.35) + Math.ceil(canvas.height * 0.15);
+  const dateTextMarginBottom = Math.ceil(canvas.height * 0.63) + Math.ceil(canvas.height * 0.25);
+  const timeTextBoxMarginBottom = Math.ceil(canvas.height * 0.565) + Math.ceil(canvas.height * 0.235);
+  const locationTextBoxMarginBottom = Math.ceil(canvas.height * 0.32) + Math.ceil(canvas.height * 0.14);
+  const attendeesTextBoxMarginBottom = Math.ceil(canvas.height * 0.1) + Math.ceil(canvas.height * 0.11);
+
+  const mainTextSize = Math.ceil(canvas.height * 0.185);
+  const mainTextLineHeight = 1;
+
+  const dateTextSize = Math.ceil(canvas.height * 0.055);
+  const timeTextSize = Math.ceil(canvas.height * 0.05);
+  const attendeesTextSize = Math.ceil(canvas.height * 0.04);
+  const attendeesTextLineHeight = 1;
+
+  const locationTextSize = Math.ceil(canvas.height * 0.055);
+  const locationTextLineHeight = 1;
+
+  const contractedByTextSize = Math.ceil(canvas.height * 0.01);
+  const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9);
+  const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03);
+
+  if (options.dateText !== null) {
+    /* BEGIN Date text render */
+
+    const dateTextWidth = canvas.width;
+
+    console.log(options);
+
+    const highlightedData = transformHighlightedText(
+      options.dateText,
+      mainTextSize,
+      dateTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    dateTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: dateTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(dateTextBox, 1);
+
+    canvas.add(dateTextBox);
+
+    const dateTextBoxTop =
+      canvas.height - dateTextBox.height - dateTextMarginBottom;
+
+      dateTextBox.top = dateTextBoxTop - highlightedData.paddingBottom;
+
+    const dateTextBoxLeft =
+      canvas.width - dateTextBox.width - textMarginRight;
+
+      dateTextBox.left = dateTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Date text render */
+  }
+
+  if (options.timeText !== null) {
+    /* BEGIN Time text render */
+
+    const timeTextWidth = canvas.width;
+
+    const highlightedData = transformHighlightedText(
+      options.timeText,
+      timeTextSize,
+      timeTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    timeTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: timeTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(timeTextBox, 1);
+
+    canvas.add(timeTextBox);
+
+    const timeTextBoxTop =
+      canvas.height - timeTextBox.height - timeTextBoxMarginBottom;
+
+    timeTextBox.top = timeTextBoxTop - highlightedData.paddingBottom;
+
+    const timeTextBoxLeft =
+      canvas.width - timeTextBox.width - textMarginRight;
+
+    timeTextBox.left = timeTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Time text render */
+  }
+
+  if (options.locationText !== null) {
+    /* BEGIN Location text render */
+
+    const locationTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.locationText,
+      locationTextSize,
+      locationTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    locationTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: locationTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      lineHeight: locationTextLineHeight,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(locationTextBox, 2);
+
+    canvas.add(locationTextBox);
+
+    canvas.renderAll();
+
+    const locationTextBoxTop =
+      canvas.height - locationTextBox.height - locationTextBoxMarginBottom + (locationTextSize * locationTextLineHeight * (locationTextBox.textLines.length - 1));
+
+    locationTextBox.top = locationTextBoxTop - highlightedData.paddingBottom;
+
+    const locationTextBoxLeft =
+      canvas.width - locationTextBox.width - textMarginRight;
+
+    locationTextBox.left = locationTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Location text render */
+  }
+
+  if (options.attendeesText !== null) {
+    /* BEGIN Attendees text render */
+
+    const attendeesTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.attendeesText,
+      attendeesTextSize,
+      attendeesTextWidth,
+      "Roboto Condensed",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    attendeesTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Roboto Condensed",
+      fontSize: attendeesTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(attendeesTextBox, 3);
+
+    canvas.add(attendeesTextBox);
+
+    const attendeesTextBoxTop =
+      canvas.height - attendeesTextBox.height - attendeesTextBoxMarginBottom + (attendeesTextSize * attendeesTextLineHeight * (attendeesTextBox.textLines.length - 1)) + ((3 - attendeesTextBox.textLines.length - 1) * attendeesTextSize * attendeesTextLineHeight);
+
+    attendeesTextBox.top = attendeesTextBoxTop - highlightedData.paddingBottom;
+
+    const attendeesTextBoxLeft =
+      canvas.width - attendeesTextBox.width - textMarginRight;
+
+      attendeesTextBox.left = attendeesTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Attendees text render */
+  }
+
+  if (options.mainText !== null) {
+    /* BEGIN Background render */
+
+    if (backgroundImage === null || options.colors.background.value != previousBackgroundImageColor.value || !canvas.getObjects().includes(backgroundImage)) {
+      backgroundImage = new Image();
+
+      await new Promise((resolve) => {
+        backgroundImage.onload = () => {
+          resolve();
+        };
+
+        if (options.colors.background.value == COLORS.black.value) {
+          backgroundImage.src = backgroundURL;
+        } else {
+          backgroundImage.src = backgroundURLInverted;
+        }
+      });
+
+      backgroundImage = new fabric.Image(backgroundImage, {
+        top: 0,
+        left: 0,
+        zIndex: 10,
+        selectable: false,
+      });
+      backgroundImage.scaleToWidth(canvas.width);
+
+      previousBackgroundImageColor = options.colors.background;
+
+      canvas.add(backgroundImage);
+    }
+
+    /* END Background render */
+
+
+    /* BEGIN Main text render */
+
+    const mainTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.mainText,
+      mainTextSize,
+      mainTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: mainTextSize,
+      lineHeight: mainTextLineHeight,
+      fill: options.colors.highlight.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(mainTextBox, 1);
+
+    canvas.add(mainTextBox);
+
+    const mainTextBoxTop =
+      canvas.height - mainTextBox.height - mainTextMarginBottom;
+
+    mainTextBox.top = mainTextBoxTop - highlightedData.paddingBottom;
+
+    const mainTextBoxLeft =
+      canvas.width - mainTextBox.width - textMarginRight;
+
+    mainTextBox.left = mainTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Main text render */
+  }
+
+  /* BEGIN Contracted by render */
+
+  if (options.contractedBy !== null) {
+    contractedByTextbox = new fabric.Textbox(options.contractedBy, {
+      left: contractedByTextSidesMargin,
+      top: contractedByTextSidesMargin,
+      width: contractedByTextMaxWidth,
+      fontFamily: "Roboto Condensed",
+      fontSize: contractedByTextSize,
+      textAlign: "left",
+      fill: options.colors.contractedByText.value,
+      selectable: false,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(contractedByTextbox, 1);
+
+    canvas.add(contractedByTextbox);
+  }
+
+  /* END Contracted by render */
+
+  /* BEGIN Main image render */
+
+  if (
+    options.mainImage !== null &&
+    (!canvas.contains(mainImage) ||
+      mainImage === null ||
+      options.mainImage.src !== mainImageSource)
+  ) {
+    if (mainImage !== null) {
+      canvas.remove(mainImage);
+    }
+
+    mainImage = new fabric.Image(options.mainImage, {
+      left: 0,
+      top: 0,
+      zIndex: 0,
+    });
+
+    mainImage.controls = {
+      ...fabric.Image.prototype.controls,
+      mtr: new fabric.Control({ visible: false }),
+    };
+
+    if (mainImage.width >= mainImage.height) {
+      mainImage.scaleToHeight(canvas.height);
+    } else {
+      mainImage.scaleToWidth(canvas.width);
+    }
+
+    canvas.add(mainImage);
+    mainImageSource = options.mainImage.src;
+    // canvas.centerObject(mainImage)
+  } else if (mainImage !== null && options.mainImage === null) {
+    canvas.remove(mainImage);
+  }
+
+  /* END Main image render */
+
+  sortObjects(canvas);
+
+  canvasPointerDownEvent = (event) => {
+    let activeObject = canvas.getActiveObject();
+
+    if (activeObject === null) {
+      return;
+    }
+
+    // if (activeObject._element.src == mainImage._element.src) {
+    // return
+    // }
+
+    canvas.remove(backgroundImage);
+    backgroundImage = null;
+  };
+
+  if (!pointerDownEventAssigned) {
+    document
+      .getElementsByClassName("upper-canvas")[0]
+      .addEventListener("pointerdown", canvasPointerDownEvent);
+
+    pointerDownEventAssigned = true;
+  }
+
+  const colors = {...options.colors};
+
+  upEventFunction = (event) => {
+    redraw(canvas, {...options, colors: colors});
+  };
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointerup", upEventFunction);
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointerout", upEventFunction);
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointercancel", upEventFunction);
+};
+
+export default redraw;
diff --git a/frontend/src/views/make_a_wish_tour_social_A2/MakeAWishTourSocialA2.vue b/frontend/src/views/make_a_wish_tour_social_A2/MakeAWishTourSocialA2.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2ef67561a47b2b84c5aad02845610394879608a0
--- /dev/null
+++ b/frontend/src/views/make_a_wish_tour_social_A2/MakeAWishTourSocialA2.vue
@@ -0,0 +1,255 @@
+<script setup>
+import { watch, ref } from "vue";
+
+import COLORS from "../../colors";
+import PEOPLE from "../../people";
+import TEMPLATES from "../../templates";
+import DEFAULT_CONTRACTOR from "../../contractors";
+import {
+  loadFonts,
+  loadCanvasStorage,
+  setCanvasStorage,
+  updateAutoRedrawStorage,
+} from "../../utils";
+
+import Canvas from "../../components/canvas/Canvas.vue";
+import redraw from "./canvas";
+
+import Navbar from "../../components/Navbar.vue";
+import MainContainer from "../../components/MainContainer.vue";
+import ImageInput from "../../components/inputs/ImageInput.vue";
+import LongTextInput from "../../components/inputs/text/LongTextInput.vue";
+import ShortTextInput from "../../components/inputs/text/ShortTextInput.vue";
+import RangeInput from "../../components/inputs/RangeInput.vue";
+import InputSeparator from "../../components/inputs/InputSeparator.vue";
+import SelectInput from "../../components/inputs/SelectInput.vue";
+import MultipleColorPicker from "../../components/inputs/colors/MultipleColorPicker.vue";
+import ReloadButton from "../../components/reload/ReloadButton.vue";
+import AutoReloadCheckbox from "../../components/reload/AutoReloadCheckbox.vue";
+</script>
+
+<script>
+await loadFonts([
+  "12px Bebas Neue",
+  "12px Roboto Condensed",
+  "bold 12px Roboto Condensed",
+]);
+
+export default {
+  components: {
+    Canvas,
+    Navbar,
+    MainContainer,
+    ImageInput,
+    LongTextInput,
+    ShortTextInput,
+    RangeInput,
+    SelectInput,
+    InputSeparator,
+    MultipleColorPicker,
+  },
+  data() {
+    const predefinedColors = {
+      base: {
+        name: "Černé pozadí",
+        colors: {
+          background: COLORS.black,
+          highlight: COLORS.yellow1,
+          baseText: COLORS.white,
+          nameText: COLORS.yellow1,
+          highlightedText: COLORS.black,
+          contractedByText: COLORS.gray1,
+        },
+      },
+      white: {
+        name: "Bílé pozadí",
+        colors: {
+          background: COLORS.white,
+          highlight: COLORS.yellow1,
+          baseText: COLORS.black,
+          nameText: COLORS.black,
+          highlightedText: COLORS.black,
+          contractedByText: COLORS.gray1,
+        },
+      },
+    };
+
+    return {
+      mainImage: null,
+      mainText: null,
+
+      dateText: null,
+      timeText: null,
+      locationText: null,
+      attendeesText: null,
+
+      personName: null,
+      personPosition: null,
+      contractedBy: DEFAULT_CONTRACTOR,
+      colorLabels: {
+        background: "Pozadí",
+        highlight: "Zvýraznění",
+        baseText: "Město",
+        highlightedText: "Zvýrazněný text",
+      },
+      predefinedColors: predefinedColors,
+      colors: predefinedColors.base.colors,
+      autoRedraw: false,
+    };
+  },
+  async created() {
+    await loadCanvasStorage(this);
+  },
+  methods: {
+    async reloadCanvasProperties() {
+      const canvasProperties = {
+        mainImage: this.mainImage,
+        mainText: this.mainText,
+
+        dateText: this.dateText,
+        timeText: this.timeText,
+        locationText: this.locationText,
+        attendeesText: this.attendeesText,
+
+        personName: this.personName,
+        personPosition: this.personPosition,
+        contractedBy: this.contractedBy,
+        colors: this.colors,
+      };
+
+      if (canvasProperties.mainText) {
+        window.fileName = canvasProperties.mainText;
+      }
+
+      await this.$refs.canvas.redraw(canvasProperties);
+
+      delete canvasProperties.colors;
+      setCanvasStorage(canvasProperties);
+    },
+  },
+  mounted() {
+    this.$watch(
+      (vm) => [
+        vm.mainImage,
+        vm.mainText,
+        vm.dateText,
+        vm.timeText,
+        vm.locationText,
+        vm.attendeesText,
+        vm.personName,
+        vm.personPosition,
+        vm.contractedBy,
+        vm.colors,
+      ],
+      async (value) => {
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+      {
+        immediate: true,
+        deep: true,
+      },
+    );
+
+    this.$watch(
+      (vm) => [vm.autoRedraw],
+      async (value) => {
+        updateAutoRedrawStorage(this.autoRedraw);
+
+        if (this.autoRedraw) {
+          await this.reloadCanvasProperties();
+        }
+      },
+    );
+  },
+};
+</script>
+
+<template>
+  <header>
+    <Navbar :defaultTemplate="TEMPLATES.basic_photo_banner"></Navbar>
+  </header>
+  <main>
+    <MainContainer>
+      <template v-slot:left>
+        <Canvas
+          ref="canvas"
+          :redrawFunction="redraw"
+          width="2480"
+          height="3580"
+        />
+      </template>
+
+      <template v-slot:right>
+        <ReloadButton :parentRefs="$refs" @click="reloadCanvasProperties" />
+        <AutoReloadCheckbox v-model="autoRedraw" />
+        <ImageInput
+          name="Obrázek"
+          v-model="mainImage"
+          :important="true"
+          zIndex="10"
+        />
+        <LongTextInput
+          name="Město konání akce"
+          v-model="mainText"
+          :important="true"
+          :highlightable="true"
+          zIndex="9"
+        />
+
+        <ShortTextInput
+          name="Datum akce"
+          v-model="dateText"
+          :important="true"
+          zIndex="8"
+        />
+
+        <ShortTextInput
+          name="Čas akce"
+          v-model="timeText"
+          :important="true"
+          zIndex="7"
+        />
+
+        <LongTextInput
+          name="Účastníci akce"
+          v-model="attendeesText"
+          :important="true"
+          zIndex="6"
+        />
+
+        <LongTextInput
+          name="Přesné místo konání akce"
+          v-model="locationText"
+          :important="true"
+          zIndex="6"
+        />
+
+        <InputSeparator />
+
+        <MultipleColorPicker
+          name="Barvy"
+          v-model="colors"
+          :important="false"
+          :colorLabels="colorLabels"
+          :predefinedColors="predefinedColors"
+          :defaultPredefinedColors="predefinedColors.base"
+          zIndex="5"
+        ></MultipleColorPicker>
+
+        <ShortTextInput
+          name="Zadavatel a zpracovatel"
+          v-model="contractedBy"
+          :defaultValue="DEFAULT_CONTRACTOR"
+          :important="false"
+          zIndex="4"
+        />
+      </template>
+    </MainContainer>
+  </main>
+</template>
+
+<style>
+@import "vue-select/dist/vue-select.css";
+</style>
diff --git a/frontend/src/views/make_a_wish_tour_social_A2/canvas.js b/frontend/src/views/make_a_wish_tour_social_A2/canvas.js
new file mode 100644
index 0000000000000000000000000000000000000000..4a43671c7773160aa0ffbd1dfafaa7d7b55b3986
--- /dev/null
+++ b/frontend/src/views/make_a_wish_tour_social_A2/canvas.js
@@ -0,0 +1,465 @@
+import { fabric } from "fabric";
+import {
+  clearObjects,
+  sortObjects,
+  transformHighlightedText,
+  checkTextBoxHeight,
+} from "../../components/canvas/utils";
+import { PaddedHighlightingTextbox } from "../../components/canvas/textbox";
+import backgroundURL from "../../assets/template/make_a_wish_tour_social_a2/background.png";
+import backgroundURLInverted from "../../assets/template/make_a_wish_banner/background_inverted.png";
+import COLORS from "../../colors";
+
+let mainTextBox = null;
+
+let attendeesTextBox = null;
+let dateTextBox = null;
+let timeTextBox = null;
+let locationTextBox = null;
+
+let mainImage = null;
+let backgroundImage = null;
+let previousBackgroundImageColor = null;
+
+let contractedByTextbox = null;
+
+let mainImageSource = null;
+
+const removeDownEventListener = () => {
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerdown", canvasPointerDownEvent);
+};
+
+let upEventFunction = null;
+let canvasPointerDownEvent = null;
+let pointerDownEventAssigned = false;
+
+const redraw = async (canvas, options) => {
+  clearObjects(
+    [
+      mainTextBox,
+      contractedByTextbox,
+      attendeesTextBox,
+      dateTextBox,
+      timeTextBox,
+      locationTextBox
+    ],
+    canvas,
+  );
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerup", upEventFunction);
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointerout", upEventFunction);
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .removeEventListener("pointercancel", upEventFunction);
+
+  canvas.preserveObjectStacking = true;
+
+  const textMarginLeft = Math.ceil(canvas.width * 0.1);
+  const textMarginRight = Math.ceil(canvas.width * 0.078);
+
+  let mainTextMarginBottom = Math.ceil(canvas.height * 0.3);
+  const dateTextMarginBottom = Math.ceil(canvas.height * 0.605);
+  const timeTextBoxMarginBottom = Math.ceil(canvas.height * 0.535);
+  const locationTextBoxMarginBottom = Math.ceil(canvas.height * 0.27);
+  const attendeesTextBoxMarginBottom = Math.ceil(canvas.height * 0.09);
+
+  const mainTextSize = Math.ceil(canvas.height * 0.175);
+  const mainTextLineHeight = 1;
+
+  const dateTextSize = Math.ceil(canvas.height * 0.046);
+  const timeTextSize = Math.ceil(canvas.height * 0.04);
+  const attendeesTextSize = Math.ceil(canvas.height * 0.027);
+  const attendeesTextLineHeight = 1;
+
+  const locationTextSize = Math.ceil(canvas.height * 0.044);
+  const locationTextLineHeight = 1;
+
+  const contractedByTextSize = Math.ceil(canvas.height * 0.015);
+  const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9);
+  const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03);
+
+  if (options.dateText !== null) {
+    /* BEGIN Date text render */
+
+    const dateTextWidth = canvas.width * 5;  // IDFK
+
+    console.log(options);
+
+    const highlightedData = transformHighlightedText(
+      options.dateText,
+      mainTextSize,
+      dateTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    dateTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: dateTextWidth,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: dateTextSize,
+      fill: options.colors.highlight.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(dateTextBox, 1);
+
+    canvas.add(dateTextBox);
+
+    const dateTextBoxTop =
+      canvas.height - dateTextBox.height - dateTextMarginBottom;
+
+      dateTextBox.top = dateTextBoxTop - highlightedData.paddingBottom;
+
+    const dateTextBoxLeft =
+      canvas.width - dateTextBox.width - textMarginRight;
+
+      dateTextBox.left = dateTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Date text render */
+  }
+
+  if (options.timeText !== null) {
+    /* BEGIN Time text render */
+
+    const timeTextWidth = canvas.width;
+
+    const highlightedData = transformHighlightedText(
+      options.timeText,
+      timeTextSize,
+      timeTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    timeTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: timeTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(timeTextBox, 1);
+
+    canvas.add(timeTextBox);
+
+    const timeTextBoxTop =
+      canvas.height - timeTextBox.height - timeTextBoxMarginBottom;
+
+    timeTextBox.top = timeTextBoxTop - highlightedData.paddingBottom;
+
+    const timeTextBoxLeft =
+      canvas.width - timeTextBox.width - textMarginRight;
+
+    timeTextBox.left = timeTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Time text render */
+  }
+
+  if (options.locationText !== null) {
+    /* BEGIN Location text render */
+
+    const locationTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.locationText,
+      locationTextSize,
+      locationTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    locationTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: locationTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      lineHeight: locationTextLineHeight,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(locationTextBox, 2);
+
+    canvas.add(locationTextBox);
+
+    canvas.renderAll();
+
+    const locationTextBoxTop =
+      canvas.height - locationTextBox.height - locationTextBoxMarginBottom + (locationTextSize * locationTextLineHeight * (locationTextBox.textLines.length - 1));
+
+    locationTextBox.top = locationTextBoxTop - highlightedData.paddingBottom;
+
+    const locationTextBoxLeft =
+      canvas.width - locationTextBox.width - textMarginRight;
+
+    locationTextBox.left = locationTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Location text render */
+  }
+
+  if (options.attendeesText !== null) {
+    /* BEGIN Attendees text render */
+
+    const attendeesTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.attendeesText,
+      attendeesTextSize,
+      attendeesTextWidth,
+      "Roboto Condensed",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    attendeesTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Roboto Condensed",
+      fontSize: attendeesTextSize,
+      fill: options.colors.baseText.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(attendeesTextBox, 3);
+
+    canvas.add(attendeesTextBox);
+
+    const attendeesTextBoxTop =
+      canvas.height - attendeesTextBox.height - attendeesTextBoxMarginBottom + (attendeesTextSize * attendeesTextLineHeight * (attendeesTextBox.textLines.length - 1));
+
+    attendeesTextBox.top = attendeesTextBoxTop - highlightedData.paddingBottom;
+
+    const attendeesTextBoxLeft =
+      canvas.width - attendeesTextBox.width - textMarginRight;
+
+      attendeesTextBox.left = attendeesTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Attendees text render */
+  }
+
+  if (options.mainText !== null) {
+    /* BEGIN Background render */
+
+    if (backgroundImage === null || options.colors.background.value != previousBackgroundImageColor.value || !canvas.getObjects().includes(backgroundImage)) {
+      backgroundImage = new Image();
+
+      await new Promise((resolve) => {
+        backgroundImage.onload = () => {
+          resolve();
+        };
+
+        if (options.colors.background.value == COLORS.black.value) {
+          backgroundImage.src = backgroundURL;
+        } else {
+          backgroundImage.src = backgroundURLInverted;
+        }
+      });
+
+      backgroundImage = new fabric.Image(backgroundImage, {
+        top: 0,
+        left: -50,
+        zIndex: 10,
+        selectable: false,
+      });
+      backgroundImage.scaleToHeight(canvas.height);
+
+      previousBackgroundImageColor = options.colors.background;
+
+      canvas.add(backgroundImage);
+    }
+
+    /* END Background render */
+
+
+    /* BEGIN Main text render */
+
+    const mainTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      options.mainText,
+      mainTextSize,
+      mainTextWidth,
+      "Bebas Neue",
+      options.colors.highlightedText.value,
+      options.colors.highlight.value,
+      { padWhenDiacritics: false, invertHighlight: true },
+    );
+
+    mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "right",
+      fontFamily: "Bebas Neue",
+      fontSize: mainTextSize,
+      lineHeight: mainTextLineHeight,
+      fill: options.colors.highlight.value,
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(mainTextBox, 1);
+
+    canvas.add(mainTextBox);
+
+    const mainTextBoxTop =
+      canvas.height - mainTextBox.height - mainTextMarginBottom;
+
+    mainTextBox.top = mainTextBoxTop - highlightedData.paddingBottom;
+
+    const mainTextBoxLeft =
+      canvas.width - mainTextBox.width - textMarginRight;
+
+    mainTextBox.left = mainTextBoxLeft;
+
+    canvas.renderAll();
+
+    /* END Main text render */
+  }
+
+  /* BEGIN Contracted by render */
+
+  if (options.contractedBy !== null) {
+    contractedByTextbox = new fabric.Textbox(options.contractedBy, {
+      left: contractedByTextSidesMargin,
+      top: canvas.height - contractedByTextSidesMargin - contractedByTextSize,
+      width: contractedByTextMaxWidth,
+      fontFamily: "Roboto Condensed",
+      fontSize: contractedByTextSize,
+      textAlign: "left",
+      fill: options.colors.contractedByText.value,
+      selectable: false,
+      zIndex: 20,
+    });
+
+    checkTextBoxHeight(contractedByTextbox, 1);
+
+    canvas.add(contractedByTextbox);
+  }
+
+  /* END Contracted by render */
+
+  /* BEGIN Main image render */
+
+  if (
+    options.mainImage !== null &&
+    (!canvas.contains(mainImage) ||
+      mainImage === null ||
+      options.mainImage.src !== mainImageSource)
+  ) {
+    if (mainImage !== null) {
+      canvas.remove(mainImage);
+    }
+
+    mainImage = new fabric.Image(options.mainImage, {
+      left: 0,
+      top: 0,
+      zIndex: 0,
+    });
+
+    mainImage.controls = {
+      ...fabric.Image.prototype.controls,
+      mtr: new fabric.Control({ visible: false }),
+    };
+
+    if (mainImage.width >= mainImage.height) {
+      mainImage.scaleToHeight(canvas.height);
+    } else {
+      mainImage.scaleToWidth(canvas.width);
+    }
+
+    canvas.add(mainImage);
+    mainImageSource = options.mainImage.src;
+    // canvas.centerObject(mainImage)
+  } else if (mainImage !== null && options.mainImage === null) {
+    canvas.remove(mainImage);
+  }
+
+  /* END Main image render */
+
+  sortObjects(canvas);
+
+  canvasPointerDownEvent = (event) => {
+    let activeObject = canvas.getActiveObject();
+
+    if (activeObject === null) {
+      return;
+    }
+
+    // if (activeObject._element.src == mainImage._element.src) {
+    // return
+    // }
+
+    canvas.remove(backgroundImage);
+    backgroundImage = null;
+  };
+
+  if (!pointerDownEventAssigned) {
+    document
+      .getElementsByClassName("upper-canvas")[0]
+      .addEventListener("pointerdown", canvasPointerDownEvent);
+
+    pointerDownEventAssigned = true;
+  }
+
+  const colors = {...options.colors};
+
+  upEventFunction = (event) => {
+    redraw(canvas, {...options, colors: colors});
+  };
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointerup", upEventFunction);
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointerout", upEventFunction);
+
+  document
+    .getElementsByClassName("upper-canvas")[0]
+    .addEventListener("pointercancel", upEventFunction);
+};
+
+export default redraw;