diff --git a/district/migrations/0240_alter_districthomepage_content.py b/district/migrations/0240_alter_districthomepage_content.py
index 787a77c6f0aa3aaf71dd01e37feaf2498e6af071..7142e4c37b912acce8a68b5f01215c33dac3ad17 100644
--- a/district/migrations/0240_alter_districthomepage_content.py
+++ b/district/migrations/0240_alter_districthomepage_content.py
@@ -10,7 +10,7 @@ from django.db import migrations
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('district', '0239_districthomepage_custom_css'),
+        ('district', '0240_alter_districthomepage_content'),
     ]
 
     operations = [
diff --git a/district/migrations/0239_alter_districtarticlepage_content_and_more.py b/district/migrations/0241_alter_districtarticlepage_content_and_more.py
similarity index 100%
rename from district/migrations/0239_alter_districtarticlepage_content_and_more.py
rename to district/migrations/0241_alter_districtarticlepage_content_and_more.py
diff --git a/district/migrations/0240_alter_districtarticlepage_content_and_more.py b/district/migrations/0242_alter_districtarticlepage_content_and_more.py
similarity index 99%
rename from district/migrations/0240_alter_districtarticlepage_content_and_more.py
rename to district/migrations/0242_alter_districtarticlepage_content_and_more.py
index 813508f879570151c8d465eb2f307ccff8777f97..c74b8e265341ccc67f7aaa73c10459ce1c2cacf6 100644
--- a/district/migrations/0240_alter_districtarticlepage_content_and_more.py
+++ b/district/migrations/0242_alter_districtarticlepage_content_and_more.py
@@ -12,7 +12,7 @@ import shared.blocks.base
 
 class Migration(migrations.Migration):
     dependencies = [
-        ("district", "0239_alter_districtarticlepage_content_and_more"),
+        ("district", "0241_alter_districtarticlepage_content_and_more"),
     ]
 
     operations = [
diff --git a/district/migrations/0241_auto_20240701_1616.py b/district/migrations/0243_auto_20240701_1616.py
similarity index 88%
rename from district/migrations/0241_auto_20240701_1616.py
rename to district/migrations/0243_auto_20240701_1616.py
index 02770a03e25a41b1868000a4f154bada564e1b6c..52e5434ed6f2d0e0d1edaf844c7af1b92a43f20c 100644
--- a/district/migrations/0241_auto_20240701_1616.py
+++ b/district/migrations/0243_auto_20240701_1616.py
@@ -17,7 +17,7 @@ def fix_menu(apps, schema_editor):
 class Migration(migrations.Migration):
 
     dependencies = [
-        ('district', '0240_alter_districthomepage_content'),
+        ('district', '0242_alter_districthomepage_content'),
     ]
 
     operations = [
diff --git a/shared/blocks/__init__.py b/shared/blocks/__init__.py
index d1bcf2914596f973d680748e03c2ee796e67842c..0e97324baf18f3d90f7dd9425efcec9ef07b4e50 100644
--- a/shared/blocks/__init__.py
+++ b/shared/blocks/__init__.py
@@ -1,3 +1,34 @@
 from .parents import *  # noqa
 from .children import *  # noqa
-from .base import *  # noqa
\ No newline at end of file
+
+
+DEFAULT_CONTENT_BLOCKS = [
+    (
+        "text",
+        blocks.RichTextBlock(
+            label="Textový editor",
+            features=RICH_TEXT_DEFAULT_FEATURES,
+            template="styleguide2/includes/atoms/text/prose_richtext.html",
+        ),
+    ),
+    ("advanced_text", AdvancedTextBlock()),
+    ("two_columns_text", ColumnsTextBlock()),
+    ("headline", HeadlineBlock()),
+    ("headline_with_picture", PictureHeadlineBlock()),
+    (
+        "table",
+        TableBlock(
+            template="styleguide2/includes/atoms/table/table.html", label="Tabulka"
+        ),
+    ),
+    ("gallery", GalleryBlock(label="Galerie")),
+    ("figure", FigureBlock()),
+    ("card", CardBlock()),
+    ("two_columns", TwoColumnBlock()),
+    ("three_columns", ThreeColumnBlock()),
+    ("youtube", YouTubeVideoBlock(label="YouTube video")),
+    ("map_point", MapPointBlock(label="Špendlík na mapě")),
+    ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+    ("button", ButtonBlock()),
+    ("button_group", ButtonGroupBlock()),
+]
\ No newline at end of file
diff --git a/shared/blocks/base.py b/shared/blocks/base.py
deleted file mode 100644
index 5ab5a34524cff8595ed15c2b9807ebfa333f3c16..0000000000000000000000000000000000000000
--- a/shared/blocks/base.py
+++ /dev/null
@@ -1,1111 +0,0 @@
-class MenuItemBlock(blocks.StructBlock):
-    title = blocks.CharBlock(
-        label="Titulek",
-        help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.",
-        required=True,
-    )
-    page = blocks.PageChooserBlock(label="Stránka", required=False)
-    link = blocks.URLBlock(label="Odkaz", required=False)
-
-    class Meta:
-        label = "Položka v menu"
-        template = "styleguide/2.3.x/menu_item.html"
-
-    def clean(self, value):
-        errors = {}
-
-        if value["page"] and value["link"]:
-            errors["page"] = ErrorList(
-                ["Stránka nemůže být vybrána současně s odkazem."]
-            )
-            errors["link"] = ErrorList(
-                ["Odkaz nemůže být vybrán současně se stránkou."]
-            )
-        if errors:
-            raise StructBlockValidationError(errors)
-        return super().clean(value)
-
-
-class MenuParentBlock(blocks.StructBlock):
-    title = blocks.CharBlock(label="Titulek", required=True)
-    menu_items = blocks.ListBlock(MenuItemBlock(), label="Položky menu")
-
-    class Meta:
-        label = "Vyskakovací menu"
-        template = "styleguide2/includes/molecules/dropdown/dropdown.html"
-
-
-class ProgramItemBlock(blocks.StructBlock):
-    title = blocks.CharBlock(label="Název", required=True)
-    completion_percentage = blocks.IntegerBlock(
-        label="Procento dokončení", required=True
-    )
-    issue_link = blocks.URLBlock(label="Odkaz na Redmine issue", required=False)
-
-
-class YouTubeVideoBlock(blocks.StructBlock):
-    poster_image = ImageChooserBlock(
-        label="Náhled videa (automatické pole)",
-        required=False,
-        help_text="Není třeba vyplňovat, náhled bude " "dohledán automaticky.",
-    )
-    video_url = blocks.URLBlock(
-        label="Odkaz na video",
-        required=False,
-        help_text="Odkaz na YouTube video bude automaticky "
-        "zkonvertován na ID videa a NEBUDE uložen.",
-    )
-    video_id = blocks.CharBlock(
-        label="ID videa (automatické pole)",
-        required=False,
-        help_text="Není třeba vyplňovat, bude automaticky " "načteno z odkazu.",
-    )
-
-    class Meta:
-        label = "YouTube video"
-        icon = "media"
-        template = "styleguide2/includes/atoms/youtube_video/youtube_video.html"
-
-    def clean(self, value):
-        errors = {}
-
-        if not value["video_url"] and not value["video_id"]:
-            errors["video_url"] = ErrorList(["Zadejte prosím odkaz na YouTube video."])
-
-        if value["video_url"]:
-            if not value["video_url"].startswith("https://youtu.be") and not value[
-                "video_url"
-            ].startswith("https://www.youtube.com"):
-                errors["video_url"] = ErrorList(
-                    [
-                        'Odkaz na video musí začínat "https://www.youtube.com" '
-                        'nebo "https://youtu.be"'
-                    ]
-                )
-
-        if value["video_id"]:
-            if not re.match("^[A-Za-z0-9_-]{11}$", value["video_id"]):
-                errors["video_url"] = ErrorList(
-                    ["Formát ID YouTube videa není validní"]
-                )
-
-        if errors:
-            raise StructBlockValidationError(errors)
-        return super().clean(value)
-
-    def get_prep_value(self, value):
-        value = super().get_prep_value(value)
-
-        if value["video_url"]:
-            value["video_id"] = self.convert_youtube_link_to_video_id(
-                value["video_url"]
-            )
-            value["poster_image"] = self.get_wagtail_image_id_for_youtube_poster(
-                value["video_id"]
-            )
-
-        value["video_url"] = ""
-
-        return value
-
-    @staticmethod
-    def convert_youtube_link_to_video_id(url):
-        reg_str = (
-            "((?<=(v|V)/)|(?<=youtu\.be/)|(?<=youtube\.com/watch(\?|\&)v=)"
-            "|(?<=embed/))([\w-]+)"
-        )
-        search_result = re.search(reg_str, url)
-
-        if search_result:
-            return search_result.group(0)
-
-        logger.warning(
-            "Nepodařilo se získat video ID z YouTube URL", extra={"url": url}
-        )
-        return ""
-
-    @classmethod
-    def get_wagtail_image_id_for_youtube_poster(cls, video_id) -> int or None:
-        image_url = "https://img.youtube.com/vi/{}/hqdefault.jpg".format(video_id)
-
-        img_path = "/tmp/{}.jpg".format(video_id)
-        urllib.request.urlretrieve(image_url, img_path)
-        file = ImageFile(open(img_path, "rb"), name=img_path)
-
-        return cls.get_image_id(file, "YT_poster_v_{}".format(video_id))
-
-    @classmethod
-    def get_image_id(cls, file: ImageFile, image_title: str) -> int:
-        try:
-            image = Image.objects.get(title=image_title)
-        except Image.DoesNotExist:
-            image = Image(
-                title=image_title, file=file, collection=cls.get_posters_collection()
-            )
-            image.save()
-        return image.id
-
-    @staticmethod
-    def get_posters_collection() -> Collection:
-        collection_name = "YouTube nahledy"
-
-        try:
-            collection = Collection.objects.get(name=collection_name)
-        except Collection.DoesNotExist:
-            root_collection = Collection.get_first_root_node()
-            collection = root_collection.add_child(name=collection_name)
-
-        return collection
-
-
-class CardBlock(blocks.StructBlock):
-    img = ImageChooserBlock(label="Obrázek", required=False)
-
-    headline = blocks.TextBlock(label="Titulek", required=False)
-
-    content = blocks.StreamBlock(
-        label="Obsah",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-        ],
-        required=False,
-    )
-
-    page = blocks.PageChooserBlock(label="Stránka", required=False)
-
-    link = blocks.URLBlock(label="Odkaz", required=False)
-
-    class Meta:
-        label = "Karta"
-        icon = "form"
-        template = "styleguide2/includes/molecules/boxes/card_box_block.html"
-
-    def clean(self, value):
-        errors = {}
-
-        if value["page"] and value["link"]:
-            errors["page"] = ErrorList(
-                ["Stránka nemůže být vybrána současně s odkazem."]
-            )
-            errors["link"] = ErrorList(
-                ["Odkaz nemůže být vybrán současně se stránkou."]
-            )
-
-        if errors:
-            raise StructBlockValidationError(errors)
-
-        return super().clean(value)
-
-
-class ButtonBlock(blocks.StructBlock):
-    title = blocks.CharBlock(label="Titulek", max_length=128, required=True)
-
-    color = blocks.ChoiceBlock(
-        choices=(
-            ("black", "Černá"),
-            ("white", "Bílá"),
-            ("pirati-yellow", "Žlutá"),
-            ("grey-125", "Světle šedá"),
-            ("blue-300", "Modrá"),
-            ("cyan-200", "Tyrkysová"),
-            ("green-400", "Zelená"),
-            ("violet-400", "Vínová"),
-            ("red-600", "Červená"),
-        ),
-        label="Barva",
-        default="black",
-    )
-
-    hoveractive = blocks.BooleanBlock(
-        label="Animovat na hover",
-        default=True,
-        help_text="Pokud je zapnuto, tlačítko při najetí kurzorem ukáže žlutou šipku.",
-        required=False,
-    )
-
-    page = blocks.PageChooserBlock(label="Stránka", required=False)
-
-    link = blocks.URLBlock(label="Odkaz", required=False)
-
-    align = blocks.ChoiceBlock(
-        choices=(
-            ("auto", "Automaticky"),
-            ("center", "Na střed"),
-        ),
-        label="Zarovnání",
-        default="auto",
-    )
-
-    class Meta:
-        label = "Tlačítko"
-        icon = "code"
-        template = "styleguide2/includes/atoms/buttons/round_button_block.html"
-
-    def get_context(self, value, parent_context=None):
-        context = super().get_context(value, parent_context)
-
-        context["background_color"] = f"bg-{value['color']}"
-
-        context["text_color"] = (
-            "text-white"
-            if value["color"]
-            in (
-                "black",
-                "red-600",
-                "blue-300",
-                "cyan-200",
-                "green-400",
-                "violet-400",
-                "red-600",
-            )
-            else "text-black"
-        )
-
-        context[
-            "color_classes"
-        ] = f"{context['background_color']} {context['text_color']}"
-
-        return context
-
-    def clean(self, value):
-        errors = {}
-
-        if value["page"] and value["link"]:
-            errors["page"] = ErrorList(
-                ["Stránka nemůže být vybrána současně s odkazem."]
-            )
-            errors["link"] = ErrorList(
-                ["Odkaz nemůže být vybrán současně se stránkou."]
-            )
-
-        if not value["page"] and not value["link"]:
-            errors["page"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])
-            errors["link"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])
-
-        if value["hoveractive"] and value["color"] not in (
-            "black",
-            "white",
-            "grey-125",
-        ):
-            errors["hoveractive"] = ErrorList(
-                ["Šipku lze ukazovat pouze s černým, bílým nebo šedým pozadím."]
-            )
-
-        if errors:
-            raise StructBlockValidationError(errors)
-
-        return super().clean(value)
-
-
-class ButtonGroupBlock(blocks.StructBlock):
-    buttons = blocks.ListBlock(ButtonBlock(), label="Tlačítka")
-
-    class Meta:
-        label = "Skupina tlačítek"
-        icon = "list-ul"
-        template = "styleguide2/includes/atoms/buttons/round_button_group_block.html"
-
-
-class FullSizeHeaderBlock(blocks.StructBlock):
-    title = blocks.CharBlock(label="Titulek", required=True)
-    image_background = ImageChooserBlock(label="Obrázek v pozadí", required=True)
-    image_foreground = ImageChooserBlock(label="Obrázek v popředí", required=False)
-    button_group = blocks.ListBlock(ButtonBlock(), label="Tlačítka")
-
-    class Meta:
-        template = "styleguide/2.3.x/blocks/full_size_header_block.html"
-        icon = "placeholder"
-        label = "Nadpis s obrázkem a tlačítky přes celou stránku"
-
-
-class HeadlineBlock(blocks.StructBlock):
-    headline = blocks.CharBlock(label="Nadpis", max_length=300, required=True)
-
-    tag = blocks.ChoiceBlock(
-        choices=(
-            ("h1", "H1"),
-            ("h2", "H2"),
-            ("h3", "H3"),
-            ("h4", "H4"),
-            ("h5", "H5"),
-            ("h6", "H6"),
-        ),
-        label="Úroveň nadpisu",
-        help_text="Čím nižší číslo, tím vyšší úroveň.",
-        required=True,
-        default="h1",
-    )
-
-    style = blocks.ChoiceBlock(
-        choices=(
-            ("head-alt-xl", "Velký, Bebas Neue - 6XL"),
-            ("head-alt-lg", "Střední, Bebas Neue - 4XL"),
-            ("head-alt-md", "Základní velikost - Roboto - MD"),
-            ("head-alt-sm", "Malý - Roboto - SM"),
-            ("head-alt-xs", "Extra malý - Roboto - XS"),
-        ),
-        label="Velikost",
-        help_text="Náhled si prohlédněte na https://styleguide2.pirati.cz/pattern/patterns/atoms/text/headings.html.",
-        default="head-alt-xl",
-        required=True,
-    )
-
-    align = blocks.ChoiceBlock(
-        choices=(
-            ("auto", "Automaticky"),
-            ("center", "Na střed"),
-        ),
-        label="Zarovnání",
-        default="auto",
-        required=True,
-    )
-
-    def get_context(self, value, parent_context=None):
-        context = super().get_context(value, parent_context)
-
-        context["responsive_style"] = {
-            "head-alt-xl": "head-6xl",
-            "head-alt-lg": "head-4xl",
-            "head-alt-md": "head-base",
-            "head-alt-sm": "head-sm",
-            "head-alt-xs": "head-xs",
-        }.get(value["style"], "head-4xl")
-
-        return context
-
-    class Meta:
-        label = "Nadpis"
-        icon = "bold"
-        template = "styleguide2/includes/atoms/text/heading.html"
-
-
-class TwoColumnBlock(blocks.StructBlock):
-    left_column_content = blocks.StreamBlock(
-        label="Obsah levého sloupce",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("card", CardBlock()),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-            ("button", ButtonBlock()),
-            ("button_group", ButtonGroupBlock()),
-        ],
-        required=True,
-    )
-    right_column_content = blocks.StreamBlock(
-        label="Obsah pravého sloupce",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("card", CardBlock()),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-            ("button", ButtonBlock()),
-            ("button_group", ButtonGroupBlock()),
-        ],
-        required=True,
-    )
-
-    class Meta:
-        label = "Dva sloupce"
-        icon = "grip"
-        template = "styleguide2/includes/atoms/grids/two_columns.html"
-
-
-class ThreeColumnBlock(blocks.StructBlock):
-    left_column_content = blocks.StreamBlock(
-        label="Obsah levého sloupce",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("card", CardBlock()),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-            ("button", ButtonBlock()),
-            ("button_group", ButtonGroupBlock()),
-        ],
-        required=True,
-    )
-    middle_column_content = blocks.StreamBlock(
-        label="Obsah prostředního sloupce",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("card", CardBlock()),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-            ("button", ButtonBlock()),
-            ("button_group", ButtonGroupBlock()),
-        ],
-        required=True,
-    )
-    right_column_content = blocks.StreamBlock(
-        label="Obsah pravého sloupce",
-        local_blocks=[
-            (
-                "text",
-                blocks.RichTextBlock(
-                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
-                ),
-            ),
-            (
-                "table",
-                TableBlock(
-                    template="styleguide2/includes/atoms/table/table.html",
-                    label="Tabulka",
-                ),
-            ),
-            ("card", CardBlock()),
-            ("figure", FigureBlock()),
-            ("youtube", YouTubeVideoBlock()),
-            ("map_point", MapPointBlock(label="Špendlík na mapě")),
-            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-            ("button", ButtonBlock()),
-            ("button_group", ButtonGroupBlock()),
-        ],
-        required=True,
-    )
-
-    class Meta:
-        label = "Tři sloupce"
-        icon = "grip"
-        template = "styleguide2/includes/atoms/grids/three_columns.html"
-
-
-class CardLinkBlockMixin(blocks.StructBlock):
-    """
-    Shared mixin for cards, which vary in templates and the types of pages
-    they're allowed to link to.
-    """
-
-    image = ImageChooserBlock(label="Obrázek")
-    title = blocks.CharBlock(label="Titulek", required=True)
-    text = blocks.RichTextBlock(label="Krátký text pod nadpisem", required=False)
-
-    page = blocks.PageChooserBlock(
-        label="Stránka",
-        page_type=[],
-        required=False,
-    )
-    link = blocks.URLBlock(label="Odkaz", required=False)
-
-    class Meta:
-        # template = ""
-        icon = "link"
-        label = "Karta s odkazem"
-
-    def clean(self, value):
-        errors = {}
-
-        if value["page"] and value["link"]:
-            errors["page"] = ErrorList(
-                ["Stránka nemůže být vybrána současně s odkazem."]
-            )
-            errors["link"] = ErrorList(
-                ["Odkaz nemůže být vybrán současně se stránkou."]
-            )
-        elif not value["page"] and not value["link"]:
-            errors["page"] = ErrorList(["Zvolte stránku nebo vyplňte odkaz."])
-            errors["link"] = ErrorList(["Vyplňte odkaz nebo zvolte stránku."])
-        if errors:
-            raise StructBlockValidationError(errors)
-        return super().clean(value)
-
-
-class CardLinkWithHeadlineBlockMixin(blocks.StructBlock):
-    headline = blocks.CharBlock(label="Titulek bloku", required=False)
-    card_items = blocks.ListBlock(CardLinkBlockMixin(), label="Karty s odkazy")
-
-    class Meta:
-        # template = ""
-        icon = "link"
-        label = "Karty odkazů s nadpisem"
-
-
-class ChartDataset(blocks.StructBlock):
-    label = blocks.CharBlock(
-        label="Označení zdroje dat",
-        max_length=120,
-    )
-
-    data = blocks.ListBlock(
-        blocks.IntegerBlock(),
-        label="Data",
-        default=[0],
-    )
-
-    class Meta:
-        label = "Zdroj dat"
-
-
-def get_redmine_projects():
-    projects = requests.get("https://redmine.pirati.cz/projects.json?limit=10000")
-
-    if projects.ok:
-        projects = projects.json()["projects"]
-    else:
-        projects = []
-
-    return [(project["id"], project["name"]) for project in projects]
-
-
-class ChartRedmineIssueDataset(blocks.StructBlock):
-    projects = blocks.MultipleChoiceBlock(
-        label="Projekty", choices=get_redmine_projects
-    )
-
-    is_open = blocks.BooleanBlock(
-        label="Jen otevřené",
-        required=False,
-    )
-    is_closed = blocks.BooleanBlock(
-        label="Jen uzavřené",
-        required=False,
-    )
-
-    created_on_min_date = blocks.DateBlock(label="Min. datum vytvoření", required=True)
-    created_on_max_date = blocks.DateBlock(label="Max. datum vytvoření", required=True)
-    updated_on = blocks.CharBlock(
-        label="Filtr pro datum aktualizace",
-        max_length=128,
-        help_text="Např. <=2023-01-01. Více informací na pi2.cz/redmine-api",
-        required=False,
-    )
-
-    issue_label = blocks.CharBlock(
-        label="Označení úkolů uvnitř grafu",
-        max_length=128,
-        required=True,
-    )
-
-    split_per_project = blocks.BooleanBlock(
-        label="Rozdělit podle projektu",
-        required=False,
-    )
-
-    only_grow = blocks.BooleanBlock(
-        label="Pouze růst nahoru",
-        required=False,
-    )
-
-    def _get_issues_url(
-        self, value, project_id: typing.Union[None, str, list[str]] = None
-    ):
-        url = "https://redmine.pirati.cz/issues.json"
-        params = [
-            ("sort", "created_on"),
-            ("limit", "100"),
-            (
-                "created_on",
-                f"><{value['created_on_min_date']}|{value['created_on_max_date']}",
-            ),
-        ]
-
-        if isinstance(project_id, str):
-            params.append(("project_id", project_id))
-        elif isinstance(project_id, list):
-            params.append(("project_id", ",".join(project_id)))
-
-        is_open = value.get("is_open", False)
-        is_closed = value.get("is_closed", False)
-
-        if is_open and is_closed:
-            params.append(("status_id", "*"))
-        elif is_open:
-            params.append(("status_id", "open"))
-        elif is_closed:
-            params.append(("status_id", "closed"))
-
-        if value.get("updated_on", "") != "":
-            params.append(("updated_on", value["updated_on"]))
-
-        is_first = True
-
-        for param_set in params:
-            param, param_value = param_set
-
-            url += "?" if is_first else "&"
-            url += f"{param}={urllib.parse.quote(param_value)}"
-
-            is_first = False
-
-        return url
-
-    def _get_parsed_issues(self, value, labels, issues_url) -> tuple:
-        issues_response = cache.get(f"redmine_{issues_url}")
-
-        if issues_response is None:
-            issues_response = requests.get(issues_url)
-            issues_response.raise_for_status()
-            issues_response = issues_response.json()
-
-            cache.set(
-                f"redmine_{issues_url}",
-                issues_response,
-                timeout=604800,  # 1 week
-            )
-
-        only_grow = value.get("only_grow", False)
-
-        collected_issues = issues_response["issues"]
-        offset = 0
-
-        while issues_response["total_count"] - offset > len(issues_response["issues"]):
-            offset += 100
-            url_with_offset = f"{issues_url}&offset={offset}"
-
-            issues_response = cache.get(f"redmine_{url_with_offset}")
-
-            if issues_response is None:
-                issues_response = requests.get(url_with_offset)
-                issues_response.raise_for_status()
-                issues_response = issues_response.json()
-
-                cache.set(
-                    f"redmine_{url_with_offset}",
-                    issues_response,
-                    timeout=604800,  # 1 week
-                )
-
-            collected_issues += issues_response["issues"]
-
-        ending_position = len(collected_issues) - 1
-
-        data = None
-
-        current_issue_count = 0
-        current_label = datetime.date.fromisoformat(
-            collected_issues[0]["created_on"].split("T")[0]
-        )
-
-        if not only_grow:
-            data = [0] * len(labels)
-
-            for position, issue in enumerate(
-                collected_issues
-            ):  # Assume correct sorting order
-                created_on_date = datetime.date.fromisoformat(
-                    issue["created_on"].split("T")[0]
-                )
-
-                if current_label != created_on_date or position == ending_position:
-                    data[
-                        labels.index(current_label)
-                    ] = current_issue_count  # Assume labels are unique
-                    current_label = created_on_date
-
-                    if position != ending_position:
-                        current_issue_count = 0
-                    else:
-                        data[labels.index(current_label)] = 1
-                        break
-
-                current_issue_count += 1
-        else:
-            data = []
-            issue_count_by_date = {}
-
-            for position, issue in enumerate(
-                collected_issues
-            ):  # Assume correct sorting order
-                created_on_date = datetime.date.fromisoformat(
-                    issue["created_on"].split("T")[0]
-                )
-
-                if current_label not in issue_count_by_date:
-                    issue_count_by_date[current_label] = 0
-
-                if current_label != created_on_date or position == ending_position:
-                    issue_count_by_date[
-                        current_label
-                    ] = current_issue_count  # Assume labels are unique
-                    current_label = created_on_date
-
-                    if position == ending_position:
-                        issue_count_by_date[current_label] = current_issue_count + 1
-                        break
-
-                current_issue_count += 1
-
-            previous_date = None
-
-            for date in labels:
-                if date not in issue_count_by_date:
-                    if previous_date is None:
-                        data.append(0)
-                        continue
-
-                    data.append(issue_count_by_date[previous_date])
-                    continue
-
-                data.append(issue_count_by_date[date])
-                previous_date = date
-
-        return data
-
-    def get_context(self, value) -> list:
-        context = super().get_context(value)
-
-        labels = []
-        datasets = []
-
-        for day_count in range(
-            (value["created_on_max_date"] - value["created_on_min_date"]).days + 1
-        ):
-            day = value["created_on_min_date"] + datetime.timedelta(days=day_count)
-            labels.append(day)
-
-        if value.get("split_per_project", False):
-            project_choices_lookup = dict(get_redmine_projects())
-
-            for project_id in value["projects"]:
-                issues_url = self._get_issues_url(value, project_id)
-
-                datasets.append(
-                    {
-                        "label": f"{value['issue_label']} - {project_choices_lookup[int(project_id)]}",
-                        "data": self._get_parsed_issues(value, labels, issues_url),
-                    }
-                )
-        else:
-            issues_url = self._get_issues_url(value, value["projects"])
-
-            datasets.append(
-                {
-                    "label": value["issue_label"],
-                    "data": self._get_parsed_issues(value, labels, issues_url),
-                }
-            )
-
-        labels = [date.strftime("%d. %m. %Y") for date in labels]
-
-        context["parsed_issue_labels"] = labels
-        context["parsed_issues"] = datasets
-
-        return context
-
-    class Meta:
-        label = "Zdroj dat z Redmine (úkoly vytvořené za den)"
-        help_text = (
-            "Po prvním otevření se bude stránka otevírat delší dobu, "
-            "zatímco se na pozadí načítají data do grafu. Poté bude "
-            "fungovat běžně."
-        )
-
-
-class ChartBlock(blocks.StructBlock):
-    title = blocks.CharBlock(
-        label="Název",
-        max_length=120,
-    )
-    chart_type = blocks.ChoiceBlock(
-        label="Typ",
-        choices=[
-            ("bar", "Graf se sloupci"),
-            ("horizontalBar", "Graf s vodorovnými sloupci"),
-            ("pie", "Koláčový graf"),
-            ("doughnut", "Donutový graf"),
-            ("polarArea", "Graf polární oblasti"),
-            ("radar", "Radarový graf"),
-            ("line", "Graf s liniemi"),
-        ],
-        default="bar",
-    )
-
-    hide_points = blocks.BooleanBlock(
-        label="Schovat body",
-        required=False,
-        help_text="Mění vzhled pouze u linových grafů.",
-    )
-
-    local_labels = blocks.ListBlock(
-        blocks.CharBlock(
-            max_length=40,
-            label="Skupina",
-        ),
-        default=[],
-        blank=True,
-        required=False,
-        collapsed=True,
-        label="Místně definované skupiny",
-    )
-    local_datasets = blocks.ListBlock(
-        ChartDataset(),
-        default=[],
-        blank=True,
-        required=False,
-        collapsed=True,
-        label="Místní zdroje dat",
-    )
-
-    redmine_issue_datasets = blocks.ListBlock(
-        ChartRedmineIssueDataset(label="Redmine úkoly"),
-        default=[],
-        blank=True,
-        required=False,
-        label="Zdroje dat z Redmine (úkoly)",
-        help_text=(
-            "Úkoly, podle doby vytvoření. Pokud definuješ "
-            "více zdrojů, datumy v nich musí být stejné."
-        ),
-    )
-
-    def clean(self, value):
-        result = super().clean(value)
-
-        redmine_issues_exist = len(value.get("redmine_issue_datasets", [])) != 0
-
-        if len(value.get("local_datasets", [])) != 0 and redmine_issues_exist:
-            raise ValidationError(
-                "Definuj pouze jeden typ zdroje dat - místní, nebo z Redmine."
-            )
-
-        if redmine_issues_exist:
-            min_date = value["redmine_issue_datasets"][0]["created_on_min_date"]
-            max_date = value["redmine_issue_datasets"][0]["created_on_max_date"]
-
-            if len(value["redmine_issue_datasets"]) > 1:
-                for dataset in value["redmine_issue_datasets"]:
-                    if (
-                        dataset["created_on_min_date"] != min_date
-                        or dataset["created_on_max_date"] != max_date
-                    ):
-                        raise ValidationError(
-                            "Maximální a minimální data všech zdrojů z Redmine musí být stejné"
-                        )
-
-        return result
-
-    def get_context(self, value, parent_context=None):
-        context = super().get_context(value, parent_context=parent_context)
-
-        datasets = []
-        labels = []
-
-        if len(value["local_datasets"]) != 0:
-            labels = value["local_labels"]
-
-            for dataset in value["local_datasets"]:
-                datasets.append(
-                    {
-                        "label": dataset["label"],
-                        "data": [item for item in dataset["data"]],
-                    }
-                )
-        elif len(value["redmine_issue_datasets"]) != 0:
-            for dataset_wrapper in value["redmine_issue_datasets"]:
-                redmine_context = ChartRedmineIssueDataset().get_context(
-                    dataset_wrapper
-                )
-
-                labels = redmine_context["parsed_issue_labels"]
-                datasets += redmine_context["parsed_issues"]
-
-        value["datasets"] = json.dumps(datasets)
-        value["labels"] = json.dumps([label for label in labels])
-
-        return context
-
-    class Meta:
-        template = "styleguide2/includes/molecules/blocks/chart.html"
-        label = "Graf"
-        icon = "form"
-        help_text = """Všechny položky zdrojů dat se chovají jako sloupce.
-Zobrazí se tolik definovaných sloupců, kolik existuje skupin."""
-
-
-class NewsletterSubscriptionBlock(blocks.StructBlock):
-    list_id = blocks.CharBlock(label="ID newsletteru", required=True)
-
-    title_line_1 = blocks.CharBlock(
-        label = "Nadpis bloku (1. řádek)",
-        required=True,
-        default="Odebírej náš"
-    )
-
-    title_line_2 = blocks.CharBlock(
-        label = "Nadpis bloku (2. řádek)",
-        required=True,
-        default="newsletter"
-    )
-
-    description = blocks.CharBlock(
-        label="Popis newsletteru",
-        required=True,
-        default="Fake news tam nenajdeš, ale dozvíš se, co chystáme doopravdy!",
-    )
-
-    class Meta:
-        label = "Formulář pro odebírání newsletteru"
-        icon = "form"
-        template = "styleguide2/includes/organisms/main_section/newsletter_section.html"
-
-
-class AdvancedTextBlock(ColorBlock, AlignBlock):
-    text = blocks.RichTextBlock(
-        label="Textový editor",
-        features=RICH_TEXT_DEFAULT_FEATURES,
-    )
-
-    class Meta:
-        label = "Textový editor (pokročilý)"
-        icon = "doc-full"
-        template = "styleguide2/includes/atoms/text/prose_advanced_richtext.html"
-
-
-class PictureHeadlineBlock(ColorBlock):
-    title = blocks.CharBlock(label="nadpis")
-    picture = ImageChooserBlock(
-        label="obrázek",
-        help_text="rozměr na výšku 75px nebo více (obrázek bude zmenšen na výšku 75px)",
-    )
-
-    class Meta:
-        label = "Nadpis (s obrázkem)"
-        icon = "title"
-        template = "styleguide2/includes/atoms/text/heading_with_image.html"
-
-
-class ColumnsTextBlock(blocks.StructBlock):
-    left_text = blocks.RichTextBlock(
-        label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
-    )
-    right_text = blocks.RichTextBlock(
-        label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
-    )
-
-    class Meta:
-        label = "Text dva sloupce"
-        icon = "doc-full"
-        template = "styleguide2/includes/atoms/text/two_columns_richtext.html"
-
-
-class AdvancedColumnsTextBlock(ColorBlock, AlignBlock):
-    left_text = blocks.RichTextBlock(
-        label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
-    )
-    right_text = blocks.RichTextBlock(
-        label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
-    )
-
-    class Meta:
-        label = "Text dva sloupce (pokročilý)"
-        icon = "doc-full"
-        template = "styleguide2/includes/atoms/text/advanced_two_columns_richtext.html"
-
-
-class PictureListBlock(ColorBlock):
-    items = blocks.ListBlock(
-        blocks.RichTextBlock(label="Odstavec", features=RICH_TEXT_DEFAULT_FEATURES),
-        label="Odstavce",
-    )
-    picture = ImageChooserBlock(
-        label="Obrázek",
-        help_text="Rozměr 30x30px nebo více (obrázek bude zmenšen na 30x30px)",
-    )
-
-    class Meta:
-        label = "Seznam z obrázkovými odrážkami"
-        icon = "list-ul"
-        template = "styleguide2/includes/molecules/lists/image_list.html"
-
-
-DEFAULT_CONTENT_BLOCKS = [
-    (
-        "text",
-        blocks.RichTextBlock(
-            label="Textový editor",
-            features=RICH_TEXT_DEFAULT_FEATURES,
-            template="styleguide2/includes/atoms/text/prose_richtext.html",
-        ),
-    ),
-    ("advanced_text", AdvancedTextBlock()),
-    ("two_columns_text", ColumnsTextBlock()),
-    ("headline", HeadlineBlock()),
-    ("headline_with_picture", PictureHeadlineBlock()),
-    (
-        "table",
-        TableBlock(
-            template="styleguide2/includes/atoms/table/table.html", label="Tabulka"
-        ),
-    ),
-    ("gallery", GalleryBlock(label="Galerie")),
-    ("figure", FigureBlock()),
-    ("card", CardBlock()),
-    ("two_columns", TwoColumnBlock()),
-    ("three_columns", ThreeColumnBlock()),
-    ("youtube", YouTubeVideoBlock(label="YouTube video")),
-    ("map_point", MapPointBlock(label="Špendlík na mapě")),
-    ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
-    ("button", ButtonBlock()),
-    ("button_group", ButtonGroupBlock()),
-]
diff --git a/shared/blocks/children/__init__.py b/shared/blocks/children/__init__.py
index 829cb54a2778d9f91bf5506c8919895fcff57823..5b480cd0b1014b47cb8313ceccd6fb860c628e3e 100644
--- a/shared/blocks/children/__init__.py
+++ b/shared/blocks/children/__init__.py
@@ -1,4 +1,6 @@
 from .candidates import *  # noqa
 from .misc import *  # noqa
 from .programs import *  # noqa
-from .mixins import *  # noqa
\ No newline at end of file
+from .mixins import *  # noqa
+from .struct import *  # noqa
+from .chart import *  # noqa
\ No newline at end of file
diff --git a/shared/blocks/children/chart.py b/shared/blocks/children/chart.py
new file mode 100644
index 0000000000000000000000000000000000000000..fb8f313dd1c5c9c8574172517e5dfad855a2fdb8
--- /dev/null
+++ b/shared/blocks/children/chart.py
@@ -0,0 +1,313 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+
+
+class ChartDataset(blocks.StructBlock):
+    label = blocks.CharBlock(
+        label="Označení zdroje dat",
+        max_length=120,
+    )
+
+    data = blocks.ListBlock(
+        blocks.IntegerBlock(),
+        label="Data",
+        default=[0],
+    )
+
+    class Meta:
+        label = "Zdroj dat"
+
+
+def get_redmine_projects():
+    projects = requests.get("https://redmine.pirati.cz/projects.json?limit=10000")
+
+    if projects.ok:
+        projects = projects.json()["projects"]
+    else:
+        projects = []
+
+    return [(project["id"], project["name"]) for project in projects]
+
+
+class ChartRedmineIssueDataset(blocks.StructBlock):
+    projects = blocks.MultipleChoiceBlock(
+        label="Projekty", choices=get_redmine_projects
+    )
+
+    is_open = blocks.BooleanBlock(
+        label="Jen otevřené",
+        required=False,
+    )
+    is_closed = blocks.BooleanBlock(
+        label="Jen uzavřené",
+        required=False,
+    )
+
+    created_on_min_date = blocks.DateBlock(label="Min. datum vytvoření", required=True)
+    created_on_max_date = blocks.DateBlock(label="Max. datum vytvoření", required=True)
+    updated_on = blocks.CharBlock(
+        label="Filtr pro datum aktualizace",
+        max_length=128,
+        help_text="Např. <=2023-01-01. Více informací na pi2.cz/redmine-api",
+        required=False,
+    )
+
+    issue_label = blocks.CharBlock(
+        label="Označení úkolů uvnitř grafu",
+        max_length=128,
+        required=True,
+    )
+
+    split_per_project = blocks.BooleanBlock(
+        label="Rozdělit podle projektu",
+        required=False,
+    )
+
+    only_grow = blocks.BooleanBlock(
+        label="Pouze růst nahoru",
+        required=False,
+    )
+
+    def _get_issues_url(
+        self, value, project_id: typing.Union[None, str, list[str]] = None
+    ):
+        url = "https://redmine.pirati.cz/issues.json"
+        params = [
+            ("sort", "created_on"),
+            ("limit", "100"),
+            (
+                "created_on",
+                f"><{value['created_on_min_date']}|{value['created_on_max_date']}",
+            ),
+        ]
+
+        if isinstance(project_id, str):
+            params.append(("project_id", project_id))
+        elif isinstance(project_id, list):
+            params.append(("project_id", ",".join(project_id)))
+
+        is_open = value.get("is_open", False)
+        is_closed = value.get("is_closed", False)
+
+        if is_open and is_closed:
+            params.append(("status_id", "*"))
+        elif is_open:
+            params.append(("status_id", "open"))
+        elif is_closed:
+            params.append(("status_id", "closed"))
+
+        if value.get("updated_on", "") != "":
+            params.append(("updated_on", value["updated_on"]))
+
+        is_first = True
+
+        for param_set in params:
+            param, param_value = param_set
+
+            url += "?" if is_first else "&"
+            url += f"{param}={urllib.parse.quote(param_value)}"
+
+            is_first = False
+
+        return url
+
+    def _get_parsed_issues(self, value, labels, issues_url) -> tuple:
+        issues_response = cache.get(f"redmine_{issues_url}")
+
+        if issues_response is None:
+            issues_response = requests.get(issues_url)
+            issues_response.raise_for_status()
+            issues_response = issues_response.json()
+
+            cache.set(
+                f"redmine_{issues_url}",
+                issues_response,
+                timeout=604800,  # 1 week
+            )
+
+        only_grow = value.get("only_grow", False)
+
+        collected_issues = issues_response["issues"]
+        offset = 0
+
+        while issues_response["total_count"] - offset > len(issues_response["issues"]):
+            offset += 100
+            url_with_offset = f"{issues_url}&offset={offset}"
+
+            issues_response = cache.get(f"redmine_{url_with_offset}")
+
+            if issues_response is None:
+                issues_response = requests.get(url_with_offset)
+                issues_response.raise_for_status()
+                issues_response = issues_response.json()
+
+                cache.set(
+                    f"redmine_{url_with_offset}",
+                    issues_response,
+                    timeout=604800,  # 1 week
+                )
+
+            collected_issues += issues_response["issues"]
+
+        ending_position = len(collected_issues) - 1
+
+        data = None
+
+        current_issue_count = 0
+        current_label = datetime.date.fromisoformat(
+            collected_issues[0]["created_on"].split("T")[0]
+        )
+
+        if not only_grow:
+            data = [0] * len(labels)
+
+            for position, issue in enumerate(
+                collected_issues
+            ):  # Assume correct sorting order
+                created_on_date = datetime.date.fromisoformat(
+                    issue["created_on"].split("T")[0]
+                )
+
+                if current_label != created_on_date or position == ending_position:
+                    data[
+                        labels.index(current_label)
+                    ] = current_issue_count  # Assume labels are unique
+                    current_label = created_on_date
+
+                    if position != ending_position:
+                        current_issue_count = 0
+                    else:
+                        data[labels.index(current_label)] = 1
+                        break
+
+                current_issue_count += 1
+        else:
+            data = []
+            issue_count_by_date = {}
+
+            for position, issue in enumerate(
+                collected_issues
+            ):  # Assume correct sorting order
+                created_on_date = datetime.date.fromisoformat(
+                    issue["created_on"].split("T")[0]
+                )
+
+                if current_label not in issue_count_by_date:
+                    issue_count_by_date[current_label] = 0
+
+                if current_label != created_on_date or position == ending_position:
+                    issue_count_by_date[
+                        current_label
+                    ] = current_issue_count  # Assume labels are unique
+                    current_label = created_on_date
+
+                    if position == ending_position:
+                        issue_count_by_date[current_label] = current_issue_count + 1
+                        break
+
+                current_issue_count += 1
+
+            previous_date = None
+
+            for date in labels:
+                if date not in issue_count_by_date:
+                    if previous_date is None:
+                        data.append(0)
+                        continue
+
+                    data.append(issue_count_by_date[previous_date])
+                    continue
+
+                data.append(issue_count_by_date[date])
+                previous_date = date
+
+        return data
+
+    def get_context(self, value) -> list:
+        context = super().get_context(value)
+
+        labels = []
+        datasets = []
+
+        for day_count in range(
+            (value["created_on_max_date"] - value["created_on_min_date"]).days + 1
+        ):
+            day = value["created_on_min_date"] + datetime.timedelta(days=day_count)
+            labels.append(day)
+
+        if value.get("split_per_project", False):
+            project_choices_lookup = dict(get_redmine_projects())
+
+            for project_id in value["projects"]:
+                issues_url = self._get_issues_url(value, project_id)
+
+                datasets.append(
+                    {
+                        "label": f"{value['issue_label']} - {project_choices_lookup[int(project_id)]}",
+                        "data": self._get_parsed_issues(value, labels, issues_url),
+                    }
+                )
+        else:
+            issues_url = self._get_issues_url(value, value["projects"])
+
+            datasets.append(
+                {
+                    "label": value["issue_label"],
+                    "data": self._get_parsed_issues(value, labels, issues_url),
+                }
+            )
+
+        labels = [date.strftime("%d. %m. %Y") for date in labels]
+
+        context["parsed_issue_labels"] = labels
+        context["parsed_issues"] = datasets
+
+        return context
+
+    class Meta:
+        label = "Zdroj dat z Redmine (úkoly vytvořené za den)"
+        help_text = (
+            "Po prvním otevření se bude stránka otevírat delší dobu, "
+            "zatímco se na pozadí načítají data do grafu. Poté bude "
+            "fungovat běžně."
+        )
\ No newline at end of file
diff --git a/shared/blocks/children/struct.py b/shared/blocks/children/struct.py
new file mode 100644
index 0000000000000000000000000000000000000000..862791a076146f2905b5473db9ab1a1a5ad08eea
--- /dev/null
+++ b/shared/blocks/children/struct.py
@@ -0,0 +1,92 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+
+
+class CardLinkBlockMixin(blocks.StructBlock):
+    """
+    Shared mixin for cards, which vary in templates and the types of pages
+    they're allowed to link to.
+    """
+
+    image = ImageChooserBlock(label="Obrázek")
+    title = blocks.CharBlock(label="Titulek", required=True)
+    text = blocks.RichTextBlock(label="Krátký text pod nadpisem", required=False)
+
+    page = blocks.PageChooserBlock(
+        label="Stránka",
+        page_type=[],
+        required=False,
+    )
+    link = blocks.URLBlock(label="Odkaz", required=False)
+
+    class Meta:
+        # template = ""
+        icon = "link"
+        label = "Karta s odkazem"
+
+    def clean(self, value):
+        errors = {}
+
+        if value["page"] and value["link"]:
+            errors["page"] = ErrorList(
+                ["Stránka nemůže být vybrána současně s odkazem."]
+            )
+            errors["link"] = ErrorList(
+                ["Odkaz nemůže být vybrán současně se stránkou."]
+            )
+        elif not value["page"] and not value["link"]:
+            errors["page"] = ErrorList(["Zvolte stránku nebo vyplňte odkaz."])
+            errors["link"] = ErrorList(["Vyplňte odkaz nebo zvolte stránku."])
+        if errors:
+            raise StructBlockValidationError(errors)
+        return super().clean(value)
+
+
+class CardLinkWithHeadlineBlockMixin(blocks.StructBlock):
+    headline = blocks.CharBlock(label="Titulek bloku", required=False)
+    card_items = blocks.ListBlock(CardLinkBlockMixin(), label="Karty s odkazy")
+
+    class Meta:
+        # template = ""
+        icon = "link"
+        label = "Karty odkazů s nadpisem"
\ No newline at end of file
diff --git a/shared/blocks/main.py b/shared/blocks/main.py
deleted file mode 100644
index 24a4338f2de36ddeb625853841f2c54ce21502cb..0000000000000000000000000000000000000000
--- a/shared/blocks/main.py
+++ /dev/null
@@ -1,421 +0,0 @@
-from django.utils.text import slugify
-from wagtail.blocks import (
-    CharBlock,
-    IntegerBlock,
-    ListBlock,
-    PageChooserBlock,
-    RichTextBlock,
-    StructBlock,
-    TextBlock,
-    URLBlock,
-)
-from wagtail.documents.blocks import DocumentChooserBlock
-from wagtail.images.blocks import ImageChooserBlock
-
-# Mixins (or used as such)
-
-
-PROGRAM_RICH_TEXT_FEATURES = [
-    "h3",
-    "h4",
-    "h5",
-    "bold",
-    "italic",
-    "ol",
-    "ul",
-    "hr",
-    "link",
-    "document-link",
-    "image",
-    "superscript",
-    "subscript",
-    "strikethrough",
-    "blockquote",
-    "embed",
-]
-
-
-class CTAMixin(StructBlock):
-    button_link = URLBlock(label="Odkaz tlačítka")
-    button_text = CharBlock(label="Text tlačítka")
-
-    class Meta:
-        icon = "doc-empty"
-        label = "Tlačítko s odkazem"
-
-
-class LinkBlock(StructBlock):
-    text = CharBlock(label="Název")
-    link = URLBlock(label="Odkaz")
-
-    class Meta:
-        icon = "link"
-        label = "Odkaz"
-
-
-# Navbar
-
-
-class NavbarMenuItemBlock(CTAMixin):
-    class Meta:
-        label = "Tlačítko"
-        template = "styleguide2/includes/molecules/navbar/additional_button.html"
-
-
-class SocialLinkBlock(LinkBlock):
-    icon = CharBlock(
-        label="Ikona",
-        help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>"
-        "Název ikony zadejte bez tečky na začátku",
-    )  # TODO CSS class name or somthing better?
-
-    class Meta:
-        icon = "link"
-        label = "Odkaz"
-
-
-# Articles
-
-
-class NewsBlock(StructBlock):
-    title = CharBlock(
-        label="Titulek",
-        help_text="Nejnovější články se načtou automaticky",
-    )
-    description = TextBlock(label="Popis", required=False)
-
-    class Meta:
-        icon = "doc-full-inverse"
-        label = "Novinky"
-
-
-class ArticleQuoteBlock(StructBlock):
-    quote = CharBlock(label="Citace")
-    autor_name = CharBlock(label="Jméno autora")
-
-    class Meta:
-        icon = "user"
-        label = "Blok citace"
-        template = "styleguide2/includes/legacy/article_quote_block.html"
-
-
-class ArticleDownloadBlock(StructBlock):
-    file = DocumentChooserBlock(label="Stáhnutelný soubor")
-
-    class Meta:
-        icon = "user"
-        label = "Blok stáhnutelného dokumentu"
-        template = "styleguide2/includes/molecules/blocks/download_block.html"
-
-
-# People
-
-
-class TwoTextColumnBlock(StructBlock):
-    text_column_1 = RichTextBlock(label="První sloupec textu")
-    text_column_2 = RichTextBlock(label="Druhý sloupec textu")
-
-    class Meta:
-        icon = "doc-full"
-        label = "Text ve dvou sloupcích"
-
-
-class PersonContactBoxBlock(StructBlock):
-    title = CharBlock(label="Titulek")
-    image = ImageChooserBlock(label="Ikona")
-    subtitle = CharBlock(label="Podtitulek")
-
-    class Meta:
-        icon = "mail"
-        label = "Kontakty"
-
-
-class PersonContactBlockMixin(StructBlock):
-    position = CharBlock(label="Název pozice", required=False)
-
-    @property
-    def person(self):
-        # NOTE: Needs to be implemented
-
-        raise NotImplementedError
-
-    # email, phone?
-
-    class Meta:
-        abstract = True
-        icon = "user"
-        label = "Osoba s volitelnou pozicí"
-
-
-# Footer
-
-
-class OtherLinksBlock(StructBlock):
-    title = CharBlock(label="Titulek")
-    list = ListBlock(LinkBlock, label="Seznam odkazů")
-
-    class Meta:
-        icon = "link"
-        label = "Odkazy"
-        template = "main/blocks/article_quote_block.html"
-
-
-class ProgramBlockPopout(StructBlock):
-    title = CharBlock(label="Titulek vyskakovacího bloku")
-    content = RichTextBlock(
-        label="Obsah",
-        features=PROGRAM_RICH_TEXT_FEATURES,
-    )
-
-    # TODO: Change in mixed-in blocks
-    guarantor = PageChooserBlock(
-        label="Garant", page_type=["district.DistrictPersonPage"], required=False
-    )
-
-    class Meta:
-        icon = "date"
-        label = "Blok programu"
-
-
-class ProgramPopoutCategory(StructBlock):
-    name = CharBlock(label="Název")
-    icon = ImageChooserBlock(label="Ikona", required=False)
-
-    description = RichTextBlock(label="Popis", required=False)
-
-    point_list = ListBlock(ProgramBlockPopout(), label="Jednotlivé bloky programu")
-
-    class Meta:
-        icon = "date"
-        label = "Kategorie programu"
-
-
-class ProgramGroupBlockPopout(StructBlock):
-    categories = ListBlock(ProgramPopoutCategory(), label="Kategorie programu")
-
-    class Meta:
-        icon = "date"
-        label = "Vyskakovací program"
-
-
-class FlipCardBlock(StructBlock):
-    bg_color = CharBlock(label="Barva pozadí", default="FEC900")
-
-    image = ImageChooserBlock(label="Obrázek", required=False)
-
-    title = TextBlock(label="Nadpis", help_text="Řádkování je manuální.")
-
-    content = RichTextBlock(label="Obsah")
-
-    button_text = CharBlock(
-        label="Nadpis tlačítka",
-        help_text="Pokud není vyplněn, tlačítko se neukáže.",
-        required=False,
-    )
-    button_url = CharBlock(label="Odkaz tlačítka", required=False)
-
-    class Meta:
-        icon = "view"
-        label = "Obracecí karta"
-        template = "styleguide2/includes/molecules/boxes/flip_card_box.html"
-
-
-class FlipCardsBlock(StructBlock):
-    cards = ListBlock(
-        FlipCardBlock(label="Karta"),
-        label="Karty",
-    )
-
-    class Meta:
-        icon = "group"
-        label = "Seznam obracecích karet"
-        template = "styleguide2/includes/organisms/cards/flip_card_list.html"
-
-
-class BoxBlock(CTAMixin, StructBlock):
-    title = CharBlock(label="Nadpis")
-    image = ImageChooserBlock(label="Logo/obrázek")
-
-    class Meta:
-        icon = "form"
-        label = "Box"
-
-
-class PeopleOverviewBlock(StructBlock):
-    title_line_1 = CharBlock(label="První řádek titulku")
-    title_line_2 = CharBlock(label="Druhý řádek titulku")
-
-    description = TextBlock(label="Popis")
-
-    list = ListBlock(BoxBlock, label="Boxíky")
-
-    class Meta:
-        template = (
-            "styleguide2/includes/organisms/main_section/representatives_section.html"
-        )
-        icon = "group"
-        label = "Skupina osob"
-
-
-# Program
-
-
-class ProgramGroupBlockMixin(StructBlock):
-    title = CharBlock(
-        label="Název programu",
-        help_text="Např. 'Krajské volby 2024', 'Evropské volby 2024', ...",
-    )
-    # point_list = ListBlock(ProgramBlock(), label="Jednotlivé články programu")
-
-    class Meta:
-        icon = "date"
-        template = "styleguide2/includes/molecules/program/program_block.html"
-        # label = "Skupina programů"
-
-    def get_prep_value(self, value):
-        value = super().get_prep_value(value)
-        value["slug"] = slugify(value["title"])
-        return value
-
-
-class ProgramBlock(StructBlock):
-    url = URLBlock(
-        label="Odkaz pokrývající celou tuto část",
-        required=False,
-    )
-    icon = ImageChooserBlock(
-        label="Ikona",
-        required=False,
-    )
-    title = CharBlock(label="Titulek článku programu")
-    text = RichTextBlock(
-        label="Obsah",
-        features=PROGRAM_RICH_TEXT_FEATURES,
-    )
-
-    class Meta:
-        icon = "date"
-        label = "Článek programu"
-
-
-class ProgramGroupBlock(ProgramGroupBlockMixin):
-    point_list = ListBlock(ProgramBlock(), label="Jednotlivé články programu")
-
-    class Meta:
-        icon = "date"
-        label = "Běžný program"
-
-
-# Candidates
-
-
-class CandidateBlock(StructBlock):
-    # NOTE: Page type should be restricted in mixed-in classes
-    page = PageChooserBlock(label="Stránka")
-
-    image = ImageChooserBlock(
-        label="Obrázek",
-        help_text="Pokud není vybrán, použije se obrázek ze stránky kandidáta",
-        required=False,
-    )
-
-    description = TextBlock(label="Popis", required=False)
-
-    class Meta:
-        template = (
-            "styleguide2/includes/molecules/candidates/candidate_primary_box.html"
-        )
-        icon = "form"
-        label = "Kandidát"
-
-
-class SecondaryCandidateBlock(StructBlock):
-    number = CharBlock(label="Číslo")
-
-    # NOTE: Page type should be restricted in mixed-in classes
-    page = PageChooserBlock(label="Stránka")
-
-    image = ImageChooserBlock(
-        label="Obrázek",
-        help_text="Pokud není vybrán, použije se obrázek ze stránky kandidáta",
-        required=False,
-    )
-
-    class Meta:
-        template = (
-            "styleguide2/includes/molecules/candidates/candidate_secondary_box.html"
-        )
-        icon = "form"
-        label = "Kandidát"
-
-
-class CandidateListBlock(StructBlock):
-    # NOTE: should be changed in mixed-in blocks.
-    candidates = ListBlock(
-        CandidateBlock(),
-        label="Kandidáti",
-    )
-
-    class Meta:
-        template = (
-            "styleguide2/includes/organisms/candidates/candidate_primary_list.html"
-        )
-        icon = "form"
-        label = "Seznam kandidátů"
-
-
-class CandidateSecondaryListBlock(StructBlock):
-    heading = CharBlock(label="Nadpis zbytku kandidátky", default="Ostatní kandidátky")
-
-    # NOTE: should be changed in mixed-in blocks.
-    candidates = ListBlock(
-        SecondaryCandidateBlock(),
-        label="Zbylí kandidáti na listině",
-    )
-
-    class Meta:
-        template = (
-            "styleguide2/includes/organisms/candidates/candidate_secondary_list.html"
-        )
-        icon = "form"
-        label = "Sekundární seznam kandidátů"
-
-
-class CarouselProgramCategoryItemBlock(StructBlock):
-    content = TextBlock(label="Obsah")
-
-    class Meta:
-        icon = "form"
-        label = "Bod"
-
-
-class CarouselProgramCategoryBlock(StructBlock):
-    number = IntegerBlock(label="Číslo")
-
-    name = CharBlock(label="Název")
-
-    points = ListBlock(CarouselProgramCategoryItemBlock(), label="Body")
-
-    class Meta:
-        icon = "form"
-        label = "Kategorie"
-
-
-class CarouselProgramBlock(StructBlock):
-    label = CharBlock(label="Nadpis", help_text="Např. 'Program'", default="Program")
-
-    categories = ListBlock(CarouselProgramCategoryBlock(), label="Kategorie")
-
-    long_version_url = URLBlock(
-        label="Odkaz na celou verzi programu",
-        help_text="Pro zobrazení odkazu na celou verzi programu musí být obě následující pole vyplněná.",
-        required=False,
-    )
-    long_version_text = CharBlock(
-        label="Nadpis odkazu na celou verzi programu", required=False
-    )
-
-    class Meta:
-        icon = "form"
-        label = "Priority programu, carousel"
-        template = "styleguide2/includes/molecules/program/card_program.html"
diff --git a/shared/blocks/parents/__init__.py b/shared/blocks/parents/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..2299fc337517a65943eaa5efad00c6c37e5a637d
--- /dev/null
+++ b/shared/blocks/parents/__init__.py
@@ -0,0 +1,7 @@
+from .text import *  # noqa
+from .programs import *  # noqa
+from .struct import *  # noqa
+from .images import *  # noqa
+from .video import *  # noqa
+from .navbar import *  # noqa
+from .button import *  # noqa
\ No newline at end of file
diff --git a/shared/blocks/parents/button.py b/shared/blocks/parents/button.py
new file mode 100644
index 0000000000000000000000000000000000000000..f782d1588dd6540ca595fddfce515879e3035f81
--- /dev/null
+++ b/shared/blocks/parents/button.py
@@ -0,0 +1,156 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class ButtonBlock(blocks.StructBlock):
+    title = blocks.CharBlock(label="Titulek", max_length=128, required=True)
+
+    color = blocks.ChoiceBlock(
+        choices=(
+            ("black", "Černá"),
+            ("white", "Bílá"),
+            ("pirati-yellow", "Žlutá"),
+            ("grey-125", "Světle šedá"),
+            ("blue-300", "Modrá"),
+            ("cyan-200", "Tyrkysová"),
+            ("green-400", "Zelená"),
+            ("violet-400", "Vínová"),
+            ("red-600", "Červená"),
+        ),
+        label="Barva",
+        default="black",
+    )
+
+    hoveractive = blocks.BooleanBlock(
+        label="Animovat na hover",
+        default=True,
+        help_text="Pokud je zapnuto, tlačítko při najetí kurzorem ukáže žlutou šipku.",
+        required=False,
+    )
+
+    page = blocks.PageChooserBlock(label="Stránka", required=False)
+
+    link = blocks.URLBlock(label="Odkaz", required=False)
+
+    align = blocks.ChoiceBlock(
+        choices=(
+            ("auto", "Automaticky"),
+            ("center", "Na střed"),
+        ),
+        label="Zarovnání",
+        default="auto",
+    )
+
+    class Meta:
+        label = "Tlačítko"
+        icon = "code"
+        template = "styleguide2/includes/atoms/buttons/round_button_block.html"
+
+    def get_context(self, value, parent_context=None):
+        context = super().get_context(value, parent_context)
+
+        context["background_color"] = f"bg-{value['color']}"
+
+        context["text_color"] = (
+            "text-white"
+            if value["color"]
+            in (
+                "black",
+                "red-600",
+                "blue-300",
+                "cyan-200",
+                "green-400",
+                "violet-400",
+                "red-600",
+            )
+            else "text-black"
+        )
+
+        context[
+            "color_classes"
+        ] = f"{context['background_color']} {context['text_color']}"
+
+        return context
+
+    def clean(self, value):
+        errors = {}
+
+        if value["page"] and value["link"]:
+            errors["page"] = ErrorList(
+                ["Stránka nemůže být vybrána současně s odkazem."]
+            )
+            errors["link"] = ErrorList(
+                ["Odkaz nemůže být vybrán současně se stránkou."]
+            )
+
+        if not value["page"] and not value["link"]:
+            errors["page"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])
+            errors["link"] = ErrorList(["Stránka nebo odkaz musí být vyplněna."])
+
+        if value["hoveractive"] and value["color"] not in (
+            "black",
+            "white",
+            "grey-125",
+        ):
+            errors["hoveractive"] = ErrorList(
+                ["Šipku lze ukazovat pouze s černým, bílým nebo šedým pozadím."]
+            )
+
+        if errors:
+            raise StructBlockValidationError(errors)
+
+        return super().clean(value)
+
+
+class ButtonGroupBlock(blocks.StructBlock):
+    buttons = blocks.ListBlock(ButtonBlock(), label="Tlačítka")
+
+    class Meta:
+        label = "Skupina tlačítek"
+        icon = "list-ul"
+        template = "styleguide2/includes/atoms/buttons/round_button_group_block.html"
\ No newline at end of file
diff --git a/shared/blocks/parents/chart.py b/shared/blocks/parents/chart.py
new file mode 100644
index 0000000000000000000000000000000000000000..ebfc6ca1382f095f45928e676966e65762fec0cc
--- /dev/null
+++ b/shared/blocks/parents/chart.py
@@ -0,0 +1,168 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class ChartBlock(blocks.StructBlock):
+    title = blocks.CharBlock(
+        label="Název",
+        max_length=120,
+    )
+    chart_type = blocks.ChoiceBlock(
+        label="Typ",
+        choices=[
+            ("bar", "Graf se sloupci"),
+            ("horizontalBar", "Graf s vodorovnými sloupci"),
+            ("pie", "Koláčový graf"),
+            ("doughnut", "Donutový graf"),
+            ("polarArea", "Graf polární oblasti"),
+            ("radar", "Radarový graf"),
+            ("line", "Graf s liniemi"),
+        ],
+        default="bar",
+    )
+
+    hide_points = blocks.BooleanBlock(
+        label="Schovat body",
+        required=False,
+        help_text="Mění vzhled pouze u linových grafů.",
+    )
+
+    local_labels = blocks.ListBlock(
+        blocks.CharBlock(
+            max_length=40,
+            label="Skupina",
+        ),
+        default=[],
+        blank=True,
+        required=False,
+        collapsed=True,
+        label="Místně definované skupiny",
+    )
+    local_datasets = blocks.ListBlock(
+        ChartDataset(),
+        default=[],
+        blank=True,
+        required=False,
+        collapsed=True,
+        label="Místní zdroje dat",
+    )
+
+    redmine_issue_datasets = blocks.ListBlock(
+        ChartRedmineIssueDataset(label="Redmine úkoly"),
+        default=[],
+        blank=True,
+        required=False,
+        label="Zdroje dat z Redmine (úkoly)",
+        help_text=(
+            "Úkoly, podle doby vytvoření. Pokud definuješ "
+            "více zdrojů, datumy v nich musí být stejné."
+        ),
+    )
+
+    def clean(self, value):
+        result = super().clean(value)
+
+        redmine_issues_exist = len(value.get("redmine_issue_datasets", [])) != 0
+
+        if len(value.get("local_datasets", [])) != 0 and redmine_issues_exist:
+            raise ValidationError(
+                "Definuj pouze jeden typ zdroje dat - místní, nebo z Redmine."
+            )
+
+        if redmine_issues_exist:
+            min_date = value["redmine_issue_datasets"][0]["created_on_min_date"]
+            max_date = value["redmine_issue_datasets"][0]["created_on_max_date"]
+
+            if len(value["redmine_issue_datasets"]) > 1:
+                for dataset in value["redmine_issue_datasets"]:
+                    if (
+                        dataset["created_on_min_date"] != min_date
+                        or dataset["created_on_max_date"] != max_date
+                    ):
+                        raise ValidationError(
+                            "Maximální a minimální data všech zdrojů z Redmine musí být stejné"
+                        )
+
+        return result
+
+    def get_context(self, value, parent_context=None):
+        context = super().get_context(value, parent_context=parent_context)
+
+        datasets = []
+        labels = []
+
+        if len(value["local_datasets"]) != 0:
+            labels = value["local_labels"]
+
+            for dataset in value["local_datasets"]:
+                datasets.append(
+                    {
+                        "label": dataset["label"],
+                        "data": [item for item in dataset["data"]],
+                    }
+                )
+        elif len(value["redmine_issue_datasets"]) != 0:
+            for dataset_wrapper in value["redmine_issue_datasets"]:
+                redmine_context = ChartRedmineIssueDataset().get_context(
+                    dataset_wrapper
+                )
+
+                labels = redmine_context["parsed_issue_labels"]
+                datasets += redmine_context["parsed_issues"]
+
+        value["datasets"] = json.dumps(datasets)
+        value["labels"] = json.dumps([label for label in labels])
+
+        return context
+
+    class Meta:
+        template = "styleguide2/includes/molecules/blocks/chart.html"
+        label = "Graf"
+        icon = "form"
+        help_text = """Všechny položky zdrojů dat se chovají jako sloupce.
+Zobrazí se tolik definovaných sloupců, kolik existuje skupin."""
\ No newline at end of file
diff --git a/shared/blocks/parents/images.py b/shared/blocks/parents/images.py
new file mode 100644
index 0000000000000000000000000000000000000000..ff0a33c8330b3d23de32202a497cb84cf9929788
--- /dev/null
+++ b/shared/blocks/parents/images.py
@@ -0,0 +1,73 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class GalleryBlock(StructBlock):
+    gallery_items = ListBlock(
+        ImageChooserBlock(label="obrázek", required=True),
+        label="Galerie",
+        icon="image",
+        group="ostatní",
+    )
+
+    class Meta:
+        label = "Galerie"
+        icon = "image"
+        template = "styleguide2/includes/molecules/gallery/gallery.html"
+
+
+class FigureBlock(StructBlock):
+    img = ImageChooserBlock(label="Obrázek", required=True)
+    caption = TextBlock(label="Popisek", required=False)
+
+    class Meta:
+        label = "Obrázek"
+        icon = "image"
+        template = "styleguide2/includes/atoms/figure/figure.html"
+  
+  
\ No newline at end of file
diff --git a/shared/blocks/parents/navbar.py b/shared/blocks/parents/navbar.py
new file mode 100644
index 0000000000000000000000000000000000000000..1b8a2f34931e11243727e71e75bedec0bff12f55
--- /dev/null
+++ b/shared/blocks/parents/navbar.py
@@ -0,0 +1,84 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class MenuItemBlock(blocks.StructBlock):
+    title = blocks.CharBlock(
+        label="Titulek",
+        help_text="Pokud není odkazovaná stránka na Majáku, použij možnost zadání samotné adresy níže.",
+        required=True,
+    )
+    page = blocks.PageChooserBlock(label="Stránka", required=False)
+    link = blocks.URLBlock(label="Odkaz", required=False)
+
+    class Meta:
+        label = "Položka v menu"
+        template = "styleguide/2.3.x/menu_item.html"
+
+    def clean(self, value):
+        errors = {}
+
+        if value["page"] and value["link"]:
+            errors["page"] = ErrorList(
+                ["Stránka nemůže být vybrána současně s odkazem."]
+            )
+            errors["link"] = ErrorList(
+                ["Odkaz nemůže být vybrán současně se stránkou."]
+            )
+        if errors:
+            raise StructBlockValidationError(errors)
+        return super().clean(value)
+
+
+class MenuParentBlock(blocks.StructBlock):
+    title = blocks.CharBlock(label="Titulek", required=True)
+    menu_items = blocks.ListBlock(MenuItemBlock(), label="Položky menu")
+
+    class Meta:
+        label = "Vyskakovací menu"
+        template = "styleguide2/includes/molecules/dropdown/dropdown.html"
\ No newline at end of file
diff --git a/shared/blocks/parents.py b/shared/blocks/parents/programs.py
similarity index 51%
rename from shared/blocks/parents.py
rename to shared/blocks/parents/programs.py
index ce57a14565bd2a0481b9705295dc62bfbb21c593..91992ce060931259697da803b7615c8f0c97fb05 100644
--- a/shared/blocks/parents.py
+++ b/shared/blocks/parents/programs.py
@@ -46,108 +46,6 @@ from .children import (
     ColorBlock, AlignBlock
 )
 
-logger = logging.getLogger(__name__)
-
-
-class SocialLinkBlock(LinkBlock):
-    icon = CharBlock(
-        label="Ikona",
-        help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>"
-        "Název ikony zadejte bez tečky na začátku",
-    )  # TODO CSS class name or somthing better?
-
-    class Meta:
-        icon = "link"
-        label = "Odkaz"
-
-
-class NewsBlock(StructBlock):
-    title = CharBlock(
-        label="Titulek",
-        help_text="Nejnovější články se načtou automaticky",
-    )
-    description = TextBlock(label="Popis", required=False)
-
-    class Meta:
-        icon = "doc-full-inverse"
-        label = "Novinky"
-
-
-class ArticleQuoteBlock(StructBlock):
-    quote = CharBlock(label="Citace")
-    autor_name = CharBlock(label="Jméno autora")
-
-    class Meta:
-        icon = "user"
-        label = "Blok citace"
-        template = "styleguide2/includes/legacy/article_quote_block.html"
-
-
-class ArticleDownloadBlock(StructBlock):
-    file = DocumentChooserBlock(label="Stáhnutelný soubor")
-
-    class Meta:
-        icon = "user"
-        label = "Blok stáhnutelného dokumentu"
-        template = "styleguide2/includes/molecules/blocks/download_block.html"
-
-
-# TODO: Merge
-class TwoTextColumnBlock(StructBlock):
-    text_column_1 = RichTextBlock(label="První sloupec textu")
-    text_column_2 = RichTextBlock(label="Druhý sloupec textu")
-
-    class Meta:
-        icon = "doc-full"
-        label = "Text ve dvou sloupcích"
-
-
-class PersonContactBoxBlock(StructBlock):
-    title = CharBlock(label="Titulek")
-    image = ImageChooserBlock(label="Ikona")
-    subtitle = CharBlock(label="Podtitulek")
-
-    class Meta:
-        icon = "mail"
-        label = "Kontakty"
-
-
-class OtherLinksBlock(StructBlock):
-    title = CharBlock(label="Titulek")
-    list = ListBlock(LinkBlock, label="Seznam odkazů")
-
-    class Meta:
-        icon = "link"
-        label = "Odkazy"
-
-
-class FlipCardsBlock(StructBlock):
-    cards = ListBlock(
-        FlipCardBlock(label="Karta"),
-        label="Karty",
-    )
-
-    class Meta:
-        icon = "group"
-        label = "Seznam obracecích karet"
-        template = "styleguide2/includes/organisms/cards/flip_card_list.html"
-
-
-class PeopleOverviewBlock(StructBlock):
-    title_line_1 = CharBlock(label="První řádek titulku")
-    title_line_2 = CharBlock(label="Druhý řádek titulku")
-
-    description = TextBlock(label="Popis")
-
-    list = ListBlock(PersonBoxBlock, label="Boxíky")
-
-    class Meta:
-        template = (
-            "styleguide2/includes/organisms/main_section/representatives_section.html"
-        )
-        icon = "group"
-        label = "Skupina osob"
-
 
 class ProgramGroupBlockPopout(StructBlock):
     categories = ListBlock(ProgramPopoutCategory(), label="Kategorie programu")
@@ -217,25 +115,9 @@ class CarouselProgramBlock(StructBlock):
         template = "styleguide2/includes/molecules/program/card_program.html"
 
 
-class GalleryBlock(StructBlock):
-    gallery_items = ListBlock(
-        ImageChooserBlock(label="obrázek", required=True),
-        label="Galerie",
-        icon="image",
-        group="ostatní",
+class ProgramItemBlock(blocks.StructBlock):
+    title = blocks.CharBlock(label="Název", required=True)
+    completion_percentage = blocks.IntegerBlock(
+        label="Procento dokončení", required=True
     )
-
-    class Meta:
-        label = "Galerie"
-        icon = "image"
-        template = "styleguide2/includes/molecules/gallery/gallery.html"
-
-
-class FigureBlock(StructBlock):
-    img = ImageChooserBlock(label="Obrázek", required=True)
-    caption = TextBlock(label="Popisek", required=False)
-
-    class Meta:
-        label = "Obrázek"
-        icon = "image"
-        template = "styleguide2/includes/atoms/figure/figure.html"
\ No newline at end of file
+    issue_link = blocks.URLBlock(label="Odkaz na Redmine issue", required=False)
\ No newline at end of file
diff --git a/shared/blocks/parents/struct.py b/shared/blocks/parents/struct.py
new file mode 100644
index 0000000000000000000000000000000000000000..92a18fe9103f45917623ba487fa8a3b015fbb229
--- /dev/null
+++ b/shared/blocks/parents/struct.py
@@ -0,0 +1,202 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+logger = logging.getLogger(__name__)
+
+
+class SocialLinkBlock(LinkBlock):
+    icon = CharBlock(
+        label="Ikona",
+        help_text="Seznam ikon - https://styleguide.pirati.cz/latest/?p=viewall-atoms-icons <br/>"
+        "Název ikony zadejte bez tečky na začátku",
+    )  # TODO CSS class name or somthing better?
+
+    class Meta:
+        icon = "link"
+        label = "Odkaz"
+
+
+class NewsBlock(StructBlock):
+    title = CharBlock(
+        label="Titulek",
+        help_text="Nejnovější články se načtou automaticky",
+    )
+    description = TextBlock(label="Popis", required=False)
+
+    class Meta:
+        icon = "doc-full-inverse"
+        label = "Novinky"
+
+
+class PersonContactBoxBlock(StructBlock):
+    title = CharBlock(label="Titulek")
+    image = ImageChooserBlock(label="Ikona")
+    subtitle = CharBlock(label="Podtitulek")
+
+    class Meta:
+        icon = "mail"
+        label = "Kontakty"
+
+
+class OtherLinksBlock(StructBlock):
+    title = CharBlock(label="Titulek")
+    list = ListBlock(LinkBlock, label="Seznam odkazů")
+
+    class Meta:
+        icon = "link"
+        label = "Odkazy"
+
+
+class FlipCardsBlock(StructBlock):
+    cards = ListBlock(
+        FlipCardBlock(label="Karta"),
+        label="Karty",
+    )
+
+    class Meta:
+        icon = "group"
+        label = "Seznam obracecích karet"
+        template = "styleguide2/includes/organisms/cards/flip_card_list.html"
+
+
+class PeopleOverviewBlock(StructBlock):
+    title_line_1 = CharBlock(label="První řádek titulku")
+    title_line_2 = CharBlock(label="Druhý řádek titulku")
+
+    description = TextBlock(label="Popis")
+
+    list = ListBlock(PersonBoxBlock, label="Boxíky")
+
+    class Meta:
+        template = (
+            "styleguide2/includes/organisms/main_section/representatives_section.html"
+        )
+        icon = "group"
+        label = "Skupina osob"
+
+
+class CardBlock(blocks.StructBlock):
+    img = ImageChooserBlock(label="Obrázek", required=False)
+
+    headline = blocks.TextBlock(label="Titulek", required=False)
+
+    content = blocks.StreamBlock(
+        label="Obsah",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+        ],
+        required=False,
+    )
+
+    page = blocks.PageChooserBlock(label="Stránka", required=False)
+
+    link = blocks.URLBlock(label="Odkaz", required=False)
+
+    class Meta:
+        label = "Karta"
+        icon = "form"
+        template = "styleguide2/includes/molecules/boxes/card_box_block.html"
+
+    def clean(self, value):
+        errors = {}
+
+        if value["page"] and value["link"]:
+            errors["page"] = ErrorList(
+                ["Stránka nemůže být vybrána současně s odkazem."]
+            )
+            errors["link"] = ErrorList(
+                ["Odkaz nemůže být vybrán současně se stránkou."]
+            )
+
+        if errors:
+            raise StructBlockValidationError(errors)
+
+        return super().clean(value)
+
+
+class NewsletterSubscriptionBlock(blocks.StructBlock):
+    list_id = blocks.CharBlock(label="ID newsletteru", required=True)
+
+    title_line_1 = blocks.CharBlock(
+        label = "Nadpis bloku (1. řádek)",
+        required=True,
+        default="Odebírej náš"
+    )
+
+    title_line_2 = blocks.CharBlock(
+        label = "Nadpis bloku (2. řádek)",
+        required=True,
+        default="newsletter"
+    )
+
+    description = blocks.CharBlock(
+        label="Popis newsletteru",
+        required=True,
+        default="Fake news tam nenajdeš, ale dozvíš se, co chystáme doopravdy!",
+    )
+
+    class Meta:
+        label = "Formulář pro odebírání newsletteru"
+        icon = "form"
+        template = "styleguide2/includes/organisms/main_section/newsletter_section.html"
\ No newline at end of file
diff --git a/shared/blocks/parents/text.py b/shared/blocks/parents/text.py
new file mode 100644
index 0000000000000000000000000000000000000000..2d75ec6726c66331ff42c84b2660af0049cb319c
--- /dev/null
+++ b/shared/blocks/parents/text.py
@@ -0,0 +1,352 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class AdvancedTextBlock(ColorBlock, AlignBlock):
+    text = blocks.RichTextBlock(
+        label="Textový editor",
+        features=RICH_TEXT_DEFAULT_FEATURES,
+    )
+
+    class Meta:
+        label = "Textový editor (pokročilý)"
+        icon = "doc-full"
+        template = "styleguide2/includes/atoms/text/prose_advanced_richtext.html"
+
+
+class ColumnsTextBlock(blocks.StructBlock):
+    left_text = blocks.RichTextBlock(
+        label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
+    )
+    right_text = blocks.RichTextBlock(
+        label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
+    )
+
+    class Meta:
+        label = "Text dva sloupce"
+        icon = "doc-full"
+        template = "styleguide2/includes/atoms/text/two_columns_richtext.html"
+
+
+# TODO: Merge
+class TwoTextColumnBlock(StructBlock):
+    text_column_1 = RichTextBlock(label="První sloupec textu")
+    text_column_2 = RichTextBlock(label="Druhý sloupec textu")
+
+    class Meta:
+        icon = "doc-full"
+        label = "Text ve dvou sloupcích"
+
+
+class ArticleQuoteBlock(StructBlock):
+    quote = CharBlock(label="Citace")
+    autor_name = CharBlock(label="Jméno autora")
+
+    class Meta:
+        icon = "user"
+        label = "Blok citace"
+        template = "styleguide2/includes/legacy/article_quote_block.html"
+
+
+class ArticleDownloadBlock(StructBlock):
+    file = DocumentChooserBlock(label="Stáhnutelný soubor")
+
+    class Meta:
+        icon = "user"
+        label = "Blok stáhnutelného dokumentu"
+        template = "styleguide2/includes/molecules/blocks/download_block.html"
+
+
+class HeadlineBlock(blocks.StructBlock):
+    headline = blocks.CharBlock(label="Nadpis", max_length=300, required=True)
+
+    tag = blocks.ChoiceBlock(
+        choices=(
+            ("h1", "H1"),
+            ("h2", "H2"),
+            ("h3", "H3"),
+            ("h4", "H4"),
+            ("h5", "H5"),
+            ("h6", "H6"),
+        ),
+        label="Úroveň nadpisu",
+        help_text="Čím nižší číslo, tím vyšší úroveň.",
+        required=True,
+        default="h1",
+    )
+
+    style = blocks.ChoiceBlock(
+        choices=(
+            ("head-alt-xl", "Velký, Bebas Neue - 6XL"),
+            ("head-alt-lg", "Střední, Bebas Neue - 4XL"),
+            ("head-alt-md", "Základní velikost - Roboto - MD"),
+            ("head-alt-sm", "Malý - Roboto - SM"),
+            ("head-alt-xs", "Extra malý - Roboto - XS"),
+        ),
+        label="Velikost",
+        help_text="Náhled si prohlédněte na https://styleguide2.pirati.cz/pattern/patterns/atoms/text/headings.html.",
+        default="head-alt-xl",
+        required=True,
+    )
+
+    align = blocks.ChoiceBlock(
+        choices=(
+            ("auto", "Automaticky"),
+            ("center", "Na střed"),
+        ),
+        label="Zarovnání",
+        default="auto",
+        required=True,
+    )
+
+    def get_context(self, value, parent_context=None):
+        context = super().get_context(value, parent_context)
+
+        context["responsive_style"] = {
+            "head-alt-xl": "head-6xl",
+            "head-alt-lg": "head-4xl",
+            "head-alt-md": "head-base",
+            "head-alt-sm": "head-sm",
+            "head-alt-xs": "head-xs",
+        }.get(value["style"], "head-4xl")
+
+        return context
+
+    class Meta:
+        label = "Nadpis"
+        icon = "bold"
+        template = "styleguide2/includes/atoms/text/heading.html"
+
+
+class TwoColumnBlock(blocks.StructBlock):
+    left_column_content = blocks.StreamBlock(
+        label="Obsah levého sloupce",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("card", CardBlock()),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+            ("button", ButtonBlock()),
+            ("button_group", ButtonGroupBlock()),
+        ],
+        required=True,
+    )
+    right_column_content = blocks.StreamBlock(
+        label="Obsah pravého sloupce",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("card", CardBlock()),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+            ("button", ButtonBlock()),
+            ("button_group", ButtonGroupBlock()),
+        ],
+        required=True,
+    )
+
+    class Meta:
+        label = "Dva sloupce"
+        icon = "grip"
+        template = "styleguide2/includes/atoms/grids/two_columns.html"
+
+
+class ThreeColumnBlock(blocks.StructBlock):
+    left_column_content = blocks.StreamBlock(
+        label="Obsah levého sloupce",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("card", CardBlock()),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+            ("button", ButtonBlock()),
+            ("button_group", ButtonGroupBlock()),
+        ],
+        required=True,
+    )
+    middle_column_content = blocks.StreamBlock(
+        label="Obsah prostředního sloupce",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("card", CardBlock()),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+            ("button", ButtonBlock()),
+            ("button_group", ButtonGroupBlock()),
+        ],
+        required=True,
+    )
+    right_column_content = blocks.StreamBlock(
+        label="Obsah pravého sloupce",
+        local_blocks=[
+            (
+                "text",
+                blocks.RichTextBlock(
+                    label="Textový editor", features=RICH_TEXT_DEFAULT_FEATURES
+                ),
+            ),
+            (
+                "table",
+                TableBlock(
+                    template="styleguide2/includes/atoms/table/table.html",
+                    label="Tabulka",
+                ),
+            ),
+            ("card", CardBlock()),
+            ("figure", FigureBlock()),
+            ("youtube", YouTubeVideoBlock()),
+            ("map_point", MapPointBlock(label="Špendlík na mapě")),
+            ("map_collection", MapFeatureCollectionBlock(label="Mapová kolekce")),
+            ("button", ButtonBlock()),
+            ("button_group", ButtonGroupBlock()),
+        ],
+        required=True,
+    )
+
+    class Meta:
+        label = "Tři sloupce"
+        icon = "grip"
+        template = "styleguide2/includes/atoms/grids/three_columns.html"
+
+
+class PictureListBlock(ColorBlock):
+    items = blocks.ListBlock(
+        blocks.RichTextBlock(label="Odstavec", features=RICH_TEXT_DEFAULT_FEATURES),
+        label="Odstavce",
+    )
+    picture = ImageChooserBlock(
+        label="Obrázek",
+        help_text="Rozměr 30x30px nebo více (obrázek bude zmenšen na 30x30px)",
+    )
+
+    class Meta:
+        label = "Seznam z obrázkovými odrážkami"
+        icon = "list-ul"
+        template = "styleguide2/includes/molecules/lists/image_list.html"
+
+
+class PictureHeadlineBlock(ColorBlock):
+    title = blocks.CharBlock(label="nadpis")
+    picture = ImageChooserBlock(
+        label="obrázek",
+        help_text="rozměr na výšku 75px nebo více (obrázek bude zmenšen na výšku 75px)",
+    )
+
+    class Meta:
+        label = "Nadpis (s obrázkem)"
+        icon = "title"
+        template = "styleguide2/includes/atoms/text/heading_with_image.html"
+
+
+class AdvancedColumnsTextBlock(ColorBlock, AlignBlock):
+    left_text = blocks.RichTextBlock(
+        label="levý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
+    )
+    right_text = blocks.RichTextBlock(
+        label="pravý sloupec", features=RICH_TEXT_DEFAULT_FEATURES
+    )
+
+    class Meta:
+        label = "Text dva sloupce (pokročilý)"
+        icon = "doc-full"
+        template = "styleguide2/includes/atoms/text/advanced_two_columns_richtext.html"
\ No newline at end of file
diff --git a/shared/blocks/parents/video.py b/shared/blocks/parents/video.py
new file mode 100644
index 0000000000000000000000000000000000000000..80d5c61ac90db132af9471d335baaff5af95bf06
--- /dev/null
+++ b/shared/blocks/parents/video.py
@@ -0,0 +1,162 @@
+from django.utils.text import slugify
+from wagtail.blocks import (
+    CharBlock,
+    IntegerBlock,
+    ListBlock,
+    PageChooserBlock,
+    RichTextBlock,
+    StructBlock,
+    TextBlock,
+    URLBlock,
+)
+from wagtail.documents.blocks import DocumentChooserBlock
+from wagtail.images.blocks import ImageChooserBlock
+import datetime
+import json
+import logging
+import re
+import typing
+import urllib
+
+import requests
+from django.core.cache import cache
+from django.core.exceptions import ValidationError
+from django.core.files.images import ImageFile
+from django.forms.utils import ErrorList
+from wagtail import blocks
+from wagtail.blocks.struct_block import StructBlockValidationError
+from wagtail.contrib.table_block.blocks import TableBlock
+from wagtail.images.blocks import ImageChooserBlock
+from wagtail.images.models import Image
+from wagtail.models import Collection
+
+from maps_utils.blocks import MapFeatureCollectionBlock, MapPointBlock
+from shared.const import (
+    ALIGN_CHOICES,
+    ALIGN_CSS,
+    BLACK_ON_WHITE,
+    COLOR_CHOICES,
+    COLOR_CSS,
+    LEFT,
+    RICH_TEXT_DEFAULT_FEATURES,
+)
+from .children import (
+    CTAMixin, LinkBlock, NavbarMenuItemBlock, PersonContactBlockMixin, ProgramBlockPopout, ProgramPopoutCategory, FlipCardBlock, PersonBoxBlock, ProgramGroupBlockMixin, ProgramPointBlock,
+    CandidateBlock, SecondaryCandidateBlock, CarouselProgramCategoryItemBlock, CarouselProgramCategoryBlock,
+    ColorBlock, AlignBlock
+)
+
+
+class YouTubeVideoBlock(blocks.StructBlock):
+    poster_image = ImageChooserBlock(
+        label="Náhled videa (automatické pole)",
+        required=False,
+        help_text="Není třeba vyplňovat, náhled bude " "dohledán automaticky.",
+    )
+    video_url = blocks.URLBlock(
+        label="Odkaz na video",
+        required=False,
+        help_text="Odkaz na YouTube video bude automaticky "
+        "zkonvertován na ID videa a NEBUDE uložen.",
+    )
+    video_id = blocks.CharBlock(
+        label="ID videa (automatické pole)",
+        required=False,
+        help_text="Není třeba vyplňovat, bude automaticky " "načteno z odkazu.",
+    )
+
+    class Meta:
+        label = "YouTube video"
+        icon = "media"
+        template = "styleguide2/includes/atoms/youtube_video/youtube_video.html"
+
+    def clean(self, value):
+        errors = {}
+
+        if not value["video_url"] and not value["video_id"]:
+            errors["video_url"] = ErrorList(["Zadejte prosím odkaz na YouTube video."])
+
+        if value["video_url"]:
+            if not value["video_url"].startswith("https://youtu.be") and not value[
+                "video_url"
+            ].startswith("https://www.youtube.com"):
+                errors["video_url"] = ErrorList(
+                    [
+                        'Odkaz na video musí začínat "https://www.youtube.com" '
+                        'nebo "https://youtu.be"'
+                    ]
+                )
+
+        if value["video_id"]:
+            if not re.match("^[A-Za-z0-9_-]{11}$", value["video_id"]):
+                errors["video_url"] = ErrorList(
+                    ["Formát ID YouTube videa není validní"]
+                )
+
+        if errors:
+            raise StructBlockValidationError(errors)
+        return super().clean(value)
+
+    def get_prep_value(self, value):
+        value = super().get_prep_value(value)
+
+        if value["video_url"]:
+            value["video_id"] = self.convert_youtube_link_to_video_id(
+                value["video_url"]
+            )
+            value["poster_image"] = self.get_wagtail_image_id_for_youtube_poster(
+                value["video_id"]
+            )
+
+        value["video_url"] = ""
+
+        return value
+
+    @staticmethod
+    def convert_youtube_link_to_video_id(url):
+        reg_str = (
+            "((?<=(v|V)/)|(?<=youtu\.be/)|(?<=youtube\.com/watch(\?|\&)v=)"
+            "|(?<=embed/))([\w-]+)"
+        )
+        search_result = re.search(reg_str, url)
+
+        if search_result:
+            return search_result.group(0)
+
+        logger.warning(
+            "Nepodařilo se získat video ID z YouTube URL", extra={"url": url}
+        )
+        return ""
+
+    @classmethod
+    def get_wagtail_image_id_for_youtube_poster(cls, video_id) -> int or None:
+        image_url = "https://img.youtube.com/vi/{}/hqdefault.jpg".format(video_id)
+
+        img_path = "/tmp/{}.jpg".format(video_id)
+        urllib.request.urlretrieve(image_url, img_path)
+        file = ImageFile(open(img_path, "rb"), name=img_path)
+
+        return cls.get_image_id(file, "YT_poster_v_{}".format(video_id))
+
+    @classmethod
+    def get_image_id(cls, file: ImageFile, image_title: str) -> int:
+        try:
+            image = Image.objects.get(title=image_title)
+        except Image.DoesNotExist:
+            image = Image(
+                title=image_title, file=file, collection=cls.get_posters_collection()
+            )
+            image.save()
+        return image.id
+
+    @staticmethod
+    def get_posters_collection() -> Collection:
+        collection_name = "YouTube nahledy"
+
+        try:
+            collection = Collection.objects.get(name=collection_name)
+        except Collection.DoesNotExist:
+            root_collection = Collection.get_first_root_node()
+            collection = root_collection.add_child(name=collection_name)
+
+        return collection
\ No newline at end of file