From 79b99f03dd410afcd2c67b434a9fae3978f7fad5 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Alexa=20Valentov=C3=A1?= <git@imaniti.org>
Date: Fri, 13 Jun 2025 12:44:00 +0200
Subject: [PATCH] update stuff

---
 frontend/src/templates.js                     |  19 +-
 .../NakopnemeBasicPhotoBannerFZBar.vue        | 254 ++++++++++++++++
 .../canvas.js                                 | 277 ++++++++++++++++++
 3 files changed, 547 insertions(+), 3 deletions(-)
 create mode 100644 frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue
 create mode 100644 frontend/src/views/nakopneme_basic_photo_banner_fzbar/canvas.js

diff --git a/frontend/src/templates.js b/frontend/src/templates.js
index 2a90c71..504e53a 100644
--- a/frontend/src/templates.js
+++ b/frontend/src/templates.js
@@ -1,6 +1,7 @@
 import basicPhotoBannerImage from "./assets/previews/basic_photo_banner.png";
 import nakopnemeBasicPhotoBannerImage from "./assets/previews/nakopneme_basic_photo_banner.png";
 import nakopnemeBasicPhotoBannerImageFZ from "./assets/previews/nakopneme_basic_photo_banner_fz.png";
+import nakopnemeBasicPhotoBannerImageFZBar from "./assets/previews/nakopneme_basic_photo_banner_fz.png";
 import urgentBasicPhotoBannerImage from "./assets/previews/urgent_basic_photo_banner.png";
 import makeawishPhotoBannerImage from "./assets/previews/make_a_wish_photo_banner.png";
 import makeawishTourSocialImage from "./assets/previews/make_a_wish_tour_social.png";
@@ -71,8 +72,8 @@ const TEMPLATES = {
       title: "Nakopneme to! Základní banner s fotkou",
     },
   },
-  nakopneme_basic_photo_banner: {
-    name: "Nakopneme to! - Základní banner s fotkou F/Z pozadi",
+  nakopneme_basic_photo_banner_fz: {
+    name: "Nakopneme to! - Základní banner s fotkou F/Ž pozadí",
     image: nakopnemeBasicPhotoBannerImageFZ,
     path: "/nakopneme-basic-photo-banner-fz",
     component: () =>
@@ -80,7 +81,19 @@ const TEMPLATES = {
         "./views/nakopneme_basic_photo_banner_fz/NakopnemeBasicPhotoBannerFZ.vue"
       ),
     meta: {
-      title: "Nakopneme to! Základní banner s fotkou - F/Z pozadi",
+      title: "Nakopneme to! Základní banner s fotkou - F/Ž pozadí",
+    },
+  },
+  nakopneme_basic_photo_banner_fzbar: {
+    name: "Nakopneme to! - Základní banner s fotkou (F/Ž bar nahoře)",
+    image: nakopnemeBasicPhotoBannerImageFZBar,
+    path: "/nakopneme-basic-photo-banner-fz-bar",
+    component: () =>
+      import(
+        "./views/nakopneme_basic_photo_banner/NakopnemeBasicPhotoBanner.vue"
+      ),
+    meta: {
+      title: "Nakopneme to! Základní banner s fotkou (F/Ž bar nahoře)",
     },
   },
   make_a_wish_banner: {
diff --git a/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue
new file mode 100644
index 0000000..c6a9a68
--- /dev/null
+++ b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/NakopnemeBasicPhotoBannerFZBar.vue
@@ -0,0 +1,254 @@
+<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 {
+  generateDefaultBadges,
+  LOGO_POSITIONS,
+  generateLogoPositions,
+} from "../../logos";
+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: "Základní barvy",
+        colors: {
+          background: COLORS.black,
+          highlight: COLORS.yellow1,
+          baseText: COLORS.white,
+          highlightedText: COLORS.black,
+          contractedByText: COLORS.gray1,
+        },
+      },
+    };
+
+    return {
+      mainImage: null,
+      mainText: null,
+      personName: null,
+      personPosition: null,
+      contractedBy: DEFAULT_CONTRACTOR,
+      logoImage: null,
+      logoPosition: LOGO_POSITIONS.top_right,
+      logoOptions: generateLogoPositions(["top_right", "top_left"]),
+      gradientHeightMultiplier: 1,
+      colorLabels: {
+        background: "Pozadí",
+        highlight: "Zvýraznění",
+        baseText: "Text",
+        highlightedText: "Zvýrazněný text",
+      },
+      predefinedColors: predefinedColors,
+      colors: predefinedColors.base.colors,
+      predefinedBadgeImages: generateDefaultBadges("defaultDark"),
+      autoRedraw: false,
+    };
+  },
+  async created() {
+    await loadCanvasStorage(this);
+  },
+  methods: {
+    async reloadCanvasProperties() {
+      const canvasProperties = {
+        mainImage: this.mainImage,
+        mainText: this.mainText,
+        logoPosition: this.logoPosition,
+        personName: this.personName,
+        personPosition: this.personPosition,
+        contractedBy: this.contractedBy,
+        logoImage: this.logoImage,
+        gradientHeightMultiplier: this.gradientHeightMultiplier,
+        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.logoPosition,
+        vm.personName,
+        vm.personPosition,
+        vm.contractedBy,
+        vm.logoImage,
+        vm.gradientHeightMultiplier,
+        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="1080"
+          height="1350"
+        />
+      </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="Hlavní text"
+          v-model="mainText"
+          :important="true"
+          :highlightable="true"
+          zIndex="9"
+        />
+        <ShortTextInput
+          name="Jméno osoby"
+          v-model="personName"
+          v-model:relatedModel="personPosition"
+          :predefinedValues="PEOPLE"
+          :important="true"
+          zIndex="8"
+        />
+        <LongTextInput
+          ref="refPersonPosition"
+          name="Pozice osoby"
+          v-model="personPosition"
+          :important="false"
+          zIndex="7"
+        />
+
+        <InputSeparator />
+
+        <ImageInput
+          name="Obrázek loga"
+          v-model="logoImage"
+          :important="false"
+          :predefinedImages="predefinedBadgeImages"
+          :mustSelectPredefinedImage="true"
+          :disableImageInput="true"
+          zIndex="7"
+        />
+
+        <SelectInput
+          name="Pozice loga"
+          :options="logoOptions"
+          v-model="logoPosition"
+          zIndex="6"
+        />
+
+        <RangeInput
+          name="Výška gradientu"
+          v-model="gradientHeightMultiplier"
+          min="0"
+          max="3"
+        />
+
+        <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/nakopneme_basic_photo_banner_fzbar/canvas.js b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/canvas.js
new file mode 100644
index 0000000..93dd7dc
--- /dev/null
+++ b/frontend/src/views/nakopneme_basic_photo_banner_fzbar/canvas.js
@@ -0,0 +1,277 @@
+import * as fabric from "fabric";
+import {
+  clearObjects,
+  sortObjects,
+  transformHighlightedText,
+  checkTextBoxHeight,
+  getSingleLineTextBoxWidth,
+} from "../../components/canvas/utils";
+import { PaddedHighlightingTextbox } from "../../components/canvas/textbox";
+import leftQuoteImage from "../../assets/template/nakopneme_basic_photo_banner/nakopneme_quote_left.png";
+import rightQuoteImage from "../../assets/template/nakopneme_basic_photo_banner/nakopneme_quote_right.png";
+
+let mainTextBox = null;
+let mainTextBoxBackground = null;
+
+let personNameText = null;
+let personInfoSeparator = null;
+let personPositionText = null;
+
+let mainImage = null;
+let logoImage = null;
+
+let contractedByTextbox = null;
+
+let mainImageSource = null;
+let previousLogoPosition = null;
+
+let leftQuote = null;
+let rightQuote = null;
+
+const redraw = async (canvas, options) => {
+  canvas.controlsAboveOverlay = true;
+
+  clearObjects(
+    [
+      mainTextBox,
+      mainTextBoxBackground,
+      personNameText,
+      personInfoSeparator,
+      personPositionText,
+      contractedByTextbox,
+      leftQuote,
+      rightQuote,
+    ],
+    canvas,
+  );
+
+  canvas.preserveObjectStacking = true;
+
+  const textMarginLeft = Math.ceil(canvas.width * 0.14);
+  const textMarginRight = Math.ceil(canvas.width * 0.075);
+
+  let mainTextMarginBottom = Math.ceil(canvas.height * 0.06);
+  const mainTextBackgroundMarginTop = Math.ceil(canvas.height * 0.1);
+  const mainTextSize = Math.ceil(canvas.height * 0.075);
+  const mainTextHeightLimit = Math.ceil(mainTextSize * 3.3);
+  const mainTextLineHeight = 0.9;
+
+  const bottomTextSize = Math.ceil(canvas.height * 0.055);
+  const nameTextMarginBottom = Math.ceil(canvas.height * 0.065);
+  const positionTextSideGap = Math.ceil(canvas.width * 0.01);
+  const positionTextSeparatorWidth = Math.ceil(canvas.width * 0.0035);
+  const positionTextMaxWidth = Math.ceil(canvas.width * 0.4);
+
+  const contractedByTextSize = Math.ceil(canvas.height * 0.02);
+  const contractedByTextMaxWidth = Math.ceil(canvas.width * 0.9);
+  const contractedByTextSidesMargin = Math.ceil(canvas.width * 0.03);
+
+  const logoWidth = Math.ceil(canvas.width * 0.13);
+  const logoSideMargin = Math.ceil(canvas.width * 0.07);
+
+  if (options.mainText !== null) {
+    /* BEGIN Main text render */
+
+    const mainText = options.mainText;
+    const mainTextWidth = canvas.width - textMarginLeft - textMarginRight;
+
+    const highlightedData = transformHighlightedText(
+      mainText,
+      mainTextSize,
+      mainTextWidth,
+      "Bebas Neue",
+      options.colors.highlight.value,
+      options.colors.highlightedText.value,
+      { padWhenDiacritics: true },
+    );
+
+    mainTextBox = new PaddedHighlightingTextbox(highlightedData.text, {
+      width: canvas.width,
+      left: 0,
+      textAlign: "center",
+      fontFamily: "Bebas Neue",
+      fontSize: mainTextSize,
+      lineHeight: mainTextLineHeight,
+      fill: "#000",
+      styles: highlightedData.styles,
+      selectable: false,
+      highlightPadding: canvas.height * 0.003,
+      zIndex: 10,
+    });
+
+    checkTextBoxHeight(mainTextBox, 4);
+
+    canvas.add(mainTextBox);
+
+    const mainTextBoxTop =
+      canvas.height - mainTextBox.height - mainTextMarginBottom;
+
+    mainTextBox.top = mainTextBoxTop - highlightedData.paddingBottom;
+
+    canvas.renderAll();
+
+    /* END Main text render */
+
+    /* BEGIN Main text background render */
+
+    const backgroundHeight =
+      canvas.height - mainTextBoxTop + mainTextBackgroundMarginTop;
+
+    mainTextBoxBackground = new fabric.Rect({
+      width: canvas.width + 30, // FIXME: Whhhhyyyyyy????
+      height: backgroundHeight + 150 * options.gradientHeightMultiplier,
+      left: -20,
+      top:
+        mainTextBoxTop -
+        mainTextBackgroundMarginTop -
+        backgroundHeight * (options.gradientHeightMultiplier - 1) -
+        150,
+      fill: new fabric.Gradient({
+        type: "linear",
+        gradientUnits: "pixels",
+        coords: {
+          x1: 0,
+          y1: 0,
+          x2: 0,
+          y2: backgroundHeight * options.gradientHeightMultiplier,
+        },
+        colorStops: [
+          {
+            offset: 0,
+            color: "#ffffff00",
+          },
+          {
+            offset: 0.5,
+            color: "#fdc800",
+          },
+          {
+            offset: 0.6,
+            color: "#fdc800",
+          },
+          {
+            offset: 1,
+            color: "#bd7eb4",
+          },
+        ],
+      }),
+      selectable: false,
+      zIndex: 9,
+    });
+
+    canvas.add(mainTextBoxBackground);
+
+    /* END Main text background render */
+  }
+
+  /* BEGIN Logo render */
+
+  // A logo is provided, and it either hasn't been rendered yet or is a new one.
+  const createNewLogo =
+    (options.logoImage !== null &&
+      (logoImage === null ||
+        (options.logoImage !== null &&
+          options.logoImage !== logoImage._element))) ||
+    previousLogoPosition != options.logoPosition.id;
+
+  previousLogoPosition = options.logoPosition.id;
+
+  if (createNewLogo) {
+    canvas.remove(logoImage);
+
+    logoImage = new fabric.Image(options.logoImage, { selectable: false });
+    logoImage.scaleToWidth(logoWidth);
+
+    if (options.logoPosition.id == "top-right") {
+      logoImage.set({
+        left: canvas.width - logoWidth - logoSideMargin,
+        top: logoSideMargin,
+        zIndex: 11,
+      });
+    } else {
+      logoImage.set({
+        left: logoSideMargin,
+        top: logoSideMargin,
+        zIndex: 11,
+      });
+    }
+
+    canvas.add(logoImage);
+  }
+
+  /* END Logo render */
+
+  /* BEGIN Contracted by render */
+
+  if (options.contractedBy !== null) {
+    contractedByTextbox = new fabric.Textbox(options.contractedBy, {
+      left:
+        canvas.width - contractedByTextMaxWidth - contractedByTextSidesMargin,
+      top: canvas.height - contractedByTextSidesMargin - contractedByTextSize,
+      width: contractedByTextMaxWidth,
+      fontFamily: "Roboto Condensed",
+      fontSize: contractedByTextSize,
+      textAlign: "right",
+      fill: "#000",
+      selectable: false,
+      zIndex: 10,
+    });
+
+    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.setControlsVisibility({
+      // corners (uniform scale)
+      tl: true,
+      tr: true,
+      bl: true,
+      br: true,
+      // mids (scale X/Y independently)
+      ml: true,
+      mr: true,
+      mt: true,
+      mb: true,
+      // rotation
+      mtr: 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);
+};
+
+export default redraw;
-- 
GitLab