diff --git a/deltatech_website_blocks/README.rst b/deltatech_website_blocks/README.rst new file mode 100644 index 000000000..0b7eca371 --- /dev/null +++ b/deltatech_website_blocks/README.rst @@ -0,0 +1,89 @@ +======================== +Deltatech Website Blocks +======================== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:567e6311c119a5113f614618940c92cd5f8da8f3b4d811aa9c6656d5144a5321 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/maturity-Alpha-red.png + :target: https://odoo-community.org/page/development-status + :alt: Alpha +.. |badge2| image:: https://img.shields.io/badge/github-dhongu%2Fdeltatech-lightgray.png?logo=github + :target: https://github.com/dhongu/deltatech/tree/18.0/deltatech_website_blocks + :alt: dhongu/deltatech + +|badge1| |badge2| + +Deltatech Website Blocks +======================== + +This module provides custom building blocks (snippets) for the Odoo +Website builder to enhance the design and functionality of your website. + +Features +======== + +1. **Partners Loop**: A dynamic snippet to display partner logos or + information in a loop. +2. **Stats Counters**: Animated counters to showcase key metrics and + statistics. +3. **Cards Grid**: A flexible grid layout for displaying content in card + format. +4. **Special Products**: A dedicated block for highlighting specific + products on your website. +5. **Countdown**: A countdown timer block (Currently in development). + +Usage +===== + +1. Enter the Website Editor. +2. Search for the new snippets in the **Blocks** tab. +3. Drag and drop the desired block onto your page. +4. Configure the block settings as needed. + +.. IMPORTANT:: + This is an alpha version, the data model and design can change at any time without warning. + Only for development or testing purpose, do not use in production. + `More details on development status `_ + +**Table of contents** + +.. contents:: + :local: + +Bug Tracker +=========== + +Bugs are tracked on `Terrabit Issues `_. +In case of trouble, please check there if your issue has already been reported. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Terrabit +* Voicu Stefan + +Maintainers +----------- + +.. |maintainer-VoicuStefan2001| image:: https://github.com/VoicuStefan2001.png?size=40px + :target: https://github.com/VoicuStefan2001 + :alt: VoicuStefan2001 + +Current maintainer: + +|maintainer-VoicuStefan2001| + +This module is part of the `dhongu/deltatech `_ project on GitHub. + +You are welcome to contribute. \ No newline at end of file diff --git a/deltatech_website_blocks/__init__.py b/deltatech_website_blocks/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/deltatech_website_blocks/__manifest__.py b/deltatech_website_blocks/__manifest__.py new file mode 100644 index 000000000..32775cc16 --- /dev/null +++ b/deltatech_website_blocks/__manifest__.py @@ -0,0 +1,37 @@ +{ + "name": "Deltatech Website Blocks", + "summary": "Custom website building blocks", + "version": "18.0.1.0.0", + "category": "Website/Website", + "author": "Terrabit, Voicu Stefan", + "website": "https://www.terrabit.ro", + "license": "OPL-1", + "depends": [ + "website", + ], + "data": [ + "views/snippets/s_partners_loop.xml", + "views/snippets/s_stats_counters.xml", + "views/snippets/s_cards_grid.xml", + "views/snippets/s_deltatech_special_products.xml", + "views/snippets/snippets.xml", + ], + "assets": { + "web.assets_frontend": [ + "deltatech_website_blocks/static/src/snippets/s_partners_loop/000.scss", + "deltatech_website_blocks/static/src/snippets/s_partners_loop/000.js", + "deltatech_website_blocks/static/src/snippets/s_stats_counters/000.scss", + "deltatech_website_blocks/static/src/snippets/s_stats_counters/000.js", + "deltatech_website_blocks/static/src/snippets/s_cards_grid/000.scss", + "deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.scss", + "deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.js", + ], + "website.assets_wysiwyg": [ + "deltatech_website_blocks/static/src/snippets/s_partners_loop/options.js", + "deltatech_website_blocks/static/src/snippets/s_stats_counters/options.js", + ], + }, + "installable": True, + "maintainers": ["VoicuStefan2001"], + "development_status": "Alpha", +} diff --git a/deltatech_website_blocks/pyproject.toml b/deltatech_website_blocks/pyproject.toml new file mode 100644 index 000000000..17730e9fc --- /dev/null +++ b/deltatech_website_blocks/pyproject.toml @@ -0,0 +1,8 @@ +[build-system] +requires = [ + "whool", +] +build-backend = "whool.buildapi" + +[project] +name = "odoo-addon-deltatech-website-blocks" diff --git a/deltatech_website_blocks/readme/DESCRIPTION.md b/deltatech_website_blocks/readme/DESCRIPTION.md new file mode 100644 index 000000000..eb471e648 --- /dev/null +++ b/deltatech_website_blocks/readme/DESCRIPTION.md @@ -0,0 +1,21 @@ +Deltatech Website Blocks +======================== + +This module provides custom building blocks (snippets) for the Odoo Website builder to enhance the design and functionality of your website. + +Features +======== + +1. **Partners Loop**: A dynamic snippet to display partner logos or information in a loop. +2. **Stats Counters**: Animated counters to showcase key metrics and statistics. +3. **Cards Grid**: A flexible grid layout for displaying content in card format. +4. **Special Products**: A dedicated block for highlighting specific products on your website. +5. **Countdown**: A countdown timer block (Currently in development). + +Usage +===== + +1. Enter the Website Editor. +2. Search for the new snippets in the **Blocks** tab. +3. Drag and drop the desired block onto your page. +4. Configure the block settings as needed. diff --git a/deltatech_website_blocks/static/description/index.html b/deltatech_website_blocks/static/description/index.html new file mode 100644 index 000000000..ec53ef8ee --- /dev/null +++ b/deltatech_website_blocks/static/description/index.html @@ -0,0 +1,433 @@ + + + + + +Deltatech Website Blocks + + + +
+

Deltatech Website Blocks

+ + +

Alpha dhongu/deltatech

+
+

Deltatech Website Blocks

+

This module provides custom building blocks (snippets) for the Odoo +Website builder to enhance the design and functionality of your website.

+
+
+

Features

+
    +
  1. Partners Loop: A dynamic snippet to display partner logos or +information in a loop.
  2. +
  3. Stats Counters: Animated counters to showcase key metrics and +statistics.
  4. +
  5. Cards Grid: A flexible grid layout for displaying content in card +format.
  6. +
  7. Special Products: A dedicated block for highlighting specific +products on your website.
  8. +
  9. Countdown: A countdown timer block (Currently in development).
  10. +
+
+
+

Usage

+
    +
  1. Enter the Website Editor.
  2. +
  3. Search for the new snippets in the Blocks tab.
  4. +
  5. Drag and drop the desired block onto your page.
  6. +
  7. Configure the block settings as needed.
  8. +
+
+

Important

+

This is an alpha version, the data model and design can change at any time without warning. +Only for development or testing purpose, do not use in production. +More details on development status

+
+

Table of contents

+
+
+

Bug Tracker

+

Bugs are tracked on Terrabit Issues. +In case of trouble, please check there if your issue has already been reported.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Terrabit
  • +
  • Voicu Stefan
  • +
+
+
+

Maintainers

+

Current maintainer:

+

VoicuStefan2001

+

This module is part of the dhongu/deltatech project on GitHub.

+

You are welcome to contribute.

+
+
+
+ + diff --git a/deltatech_website_blocks/static/src/snippets/s_cards_grid/000.scss b/deltatech_website_blocks/static/src/snippets/s_cards_grid/000.scss new file mode 100644 index 000000000..877af3779 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_cards_grid/000.scss @@ -0,0 +1,402 @@ +.s_deltatech_cards_grid { + /* Palette */ + --cpw-bg-soft: #f6f7fb; + --cpw-card: rgba(255, 255, 255, 0.78); + --cpw-card-border: rgba(255, 255, 255, 0.6); + --cpw-text: #0f172a; + --cpw-muted: #5b6478; + --cpw-line: rgba(15, 23, 42, 0.08); + + /* Primary colors */ + --cpw-primary-1: #3b82f6; /* primary color */ + --cpw-primary-2: #2563eb; /* dark (hover/contrast) */ + --cpw-primary-3: #60a5fa; /* light (highlight) */ + --cpw-primary-soft: rgba(59, 130, 246, 0.1); + --cpw-primary-line: rgba(59, 130, 246, 0.28); + + /* Neutrals */ + --cpw-ink-1: #1a1f2b; + --cpw-ink-2: #2c3340; + + /* Effects */ + --cpw-radius: 20px; + --cpw-gap: 24px; + --cpw-shadow-sm: 0 6px 18px rgba(15, 23, 42, 0.06); + --cpw-shadow: 0 18px 40px rgba(15, 23, 42, 0.12); + --cpw-shadow-lg: 0 30px 60px rgba(37, 99, 235, 0.18); + --cpw-blur: saturate(140%) blur(14px); + + color: var(--cpw-text); + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + .cpw-wrap { + position: relative; + max-width: 1220px; + margin: 0 auto; + padding: 56px 22px 72px; + border-radius: 28px; + overflow: hidden; + isolation: isolate; + background: radial-gradient(1200px 480px at 12% -10%, rgba(59, 130, 246, 0.16), transparent 60%), + radial-gradient(900px 420px at 110% 0%, rgba(26, 31, 43, 0.08), transparent 60%), + linear-gradient(180deg, #ffffff 0%, var(--cpw-bg-soft) 100%); + } + + /* Subtle top line */ + .cpw-wrap::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 2px; + background: linear-gradient( + 90deg, + transparent 0%, + rgba(59, 130, 246, 0) 8%, + rgba(59, 130, 246, 0.55) 35%, + rgba(96, 165, 250, 0.95) 50%, + rgba(59, 130, 246, 0.55) 65%, + rgba(59, 130, 246, 0) 92%, + transparent 100% + ); + z-index: 2; + } + + /* Decorative blobs */ + .cpw-blob { + position: absolute; + z-index: 0; + filter: blur(60px); + opacity: 0.5; + pointer-events: none; + } + + .cpw-blob.b1 { + width: 380px; + height: 380px; + left: -120px; + top: -80px; + background: radial-gradient(circle at 30% 30%, #60a5fa, transparent 60%); + } + + .cpw-blob.b2 { + width: 520px; + height: 520px; + right: -160px; + bottom: -180px; + background: radial-gradient(circle at 70% 70%, #93c5fd, transparent 60%); + } + + /* Section header */ + .cpw-head { + position: relative; + z-index: 2; + text-align: center; + margin: 0 auto 36px; + max-width: 760px; + } + + .cpw-eyebrow { + display: inline-flex; + align-items: center; + gap: 10px; + padding: 8px 14px; + border-radius: 999px; + background: var(--cpw-primary-soft); + border: 1px solid var(--cpw-primary-line); + color: var(--cpw-primary-2); + font-size: 12px; + font-weight: 700; + letter-spacing: 0.14em; + text-transform: uppercase; + } + + .cpw-eyebrow::before { + content: ""; + width: 6px; + height: 6px; + border-radius: 50%; + background: linear-gradient(135deg, var(--cpw-primary-3), var(--cpw-primary-1)); + box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.18); + } + + .cpw-h1 { + margin: 14px 0 10px; + font-size: clamp(26px, 3.2vw, 38px); + line-height: 1.15; + letter-spacing: -0.02em; + font-weight: 800; + color: var(--cpw-ink-1); + } + + .cpw-h1 .cpw-accent { + background: linear-gradient(135deg, var(--cpw-primary-2), var(--cpw-primary-1) 55%, var(--cpw-primary-3)); + -webkit-background-clip: text; + background-clip: text; + color: transparent; + } + + .cpw-sub { + margin: 0; + color: var(--cpw-muted); + font-size: clamp(14px, 1.4vw, 16px); + line-height: 1.6; + } + + .cpw-divider { + margin: 18px auto 0; + width: 64px; + height: 3px; + border-radius: 3px; + background: linear-gradient(90deg, var(--cpw-primary-1), var(--cpw-primary-3), var(--cpw-primary-1)); + opacity: 0.9; + } + + /* Glass panel */ + .cpw-panel { + position: relative; + z-index: 1; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.6), rgba(255, 255, 255, 0.4)); + border: 1px solid rgba(255, 255, 255, 0.7); + border-radius: 24px; + padding: 28px; + backdrop-filter: var(--cpw-blur); + -webkit-backdrop-filter: var(--cpw-blur); + box-shadow: 0 1px 0 rgba(255, 255, 255, 0.8) inset, 0 30px 60px rgba(15, 23, 42, 0.08); + } + + /* Card */ + .cpw-card { + position: relative; + background: var(--cpw-card); + border: 1px solid var(--cpw-card-border); + border-radius: var(--cpw-radius); + overflow: hidden; + backdrop-filter: var(--cpw-blur); + -webkit-backdrop-filter: var(--cpw-blur); + box-shadow: var(--cpw-shadow-sm); + cursor: pointer; + transition: transform 0.35s cubic-bezier(0.2, 0.7, 0.2, 1), box-shadow 0.35s ease, border-color 0.35s ease; + display: flex; + flex-direction: column; + min-height: 380px; + outline: none; + text-decoration: none; + color: inherit; + isolation: isolate; + margin-bottom: var(--cpw-gap); + } + + /* Subtle top line on card (appears on hover) */ + .cpw-card::after { + content: ""; + position: absolute; + left: 0; + right: 0; + top: 0; + height: 3px; + background: linear-gradient(90deg, var(--cpw-primary-2), var(--cpw-primary-1) 50%, var(--cpw-primary-3)); + transform: scaleX(0); + transform-origin: left center; + transition: transform 0.4s cubic-bezier(0.2, 0.7, 0.2, 1); + z-index: 3; + } + + &:not(.editor_enable) .cpw-card:hover::after { + transform: scaleX(1); + } + + /* Animated primary border (gradient mask) */ + .cpw-card::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + padding: 1px; + background: linear-gradient( + 135deg, + rgba(59, 130, 246, 0) 0%, + rgba(59, 130, 246, 0) 35%, + rgba(96, 165, 250, 0.85) 50%, + rgba(59, 130, 246, 0) 65%, + rgba(59, 130, 246, 0) 100% + ); + -webkit-mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + mask: linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0); + -webkit-mask-composite: xor; + mask-composite: exclude; + opacity: 0; + transition: opacity 0.35s ease; + pointer-events: none; + z-index: 2; + } + + &:not(.editor_enable) .cpw-card:hover::before { + opacity: 1; + } + + &:not(.editor_enable) .cpw-card:hover { + transform: translateY(-6px); + box-shadow: var(--cpw-shadow-lg); + border-color: var(--cpw-primary-line); + } + + .cpw-card:focus-visible { + box-shadow: 0 0 0 4px rgba(59, 130, 246, 0.22), var(--cpw-shadow); + border-color: var(--cpw-primary-1); + } + + /* Thumb */ + .cpw-thumb { + position: relative; + height: 240px; + background: radial-gradient(120% 80% at 50% 0%, rgba(59, 130, 246, 0.07), transparent 60%), + linear-gradient(180deg, #ffffff 0%, #f4f6fb 100%); + overflow: hidden; + display: flex; + align-items: center; + justify-content: center; + padding: 18px; + border-bottom: 1px solid var(--cpw-line); + } + + .cpw-thumb img { + max-width: 100%; + max-height: 100%; + width: auto; + height: auto; + object-fit: contain; + display: block; + filter: drop-shadow(0 8px 18px rgba(15, 23, 42, 0.1)); + transition: transform 0.5s cubic-bezier(0.2, 0.7, 0.2, 1); + } + + &:not(.editor_enable) .cpw-card:hover .cpw-thumb img { + transform: scale(1.04); + } + + /* "Official Document" corner badge */ + .cpw-corner { + position: absolute; + top: 12px; + right: 12px; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 11px; + font-size: 11px; + font-weight: 800; + letter-spacing: 0.14em; + text-transform: uppercase; + color: #fff; + background: linear-gradient(135deg, var(--cpw-primary-1), var(--cpw-primary-2)); + border: 1px solid rgba(255, 255, 255, 0.25); + border-radius: 999px; + box-shadow: 0 6px 14px rgba(59, 130, 246, 0.35), 0 1px 0 rgba(255, 255, 255, 0.25) inset; + z-index: 3; + + svg { + width: 12px; + height: 12px; + } + } + + /* Custom badge */ + .cpw-badge { + position: absolute; + left: 14px; + bottom: 14px; + background: rgba(26, 31, 43, 0.88); + color: #fff; + border: 1px solid rgba(255, 255, 255, 0.18); + padding: 7px 12px; + border-radius: 999px; + font-weight: 700; + font-size: 11px; + letter-spacing: 0.14em; + text-transform: uppercase; + backdrop-filter: blur(6px); + z-index: 3; + } + + /* Body */ + .cpw-card-body { + padding: 20px 20px 22px; + display: flex; + flex-direction: column; + gap: 12px; + flex: 1; + background: linear-gradient(180deg, rgba(255, 255, 255, 0.65), rgba(255, 255, 255, 0.9)); + } + + .cpw-title { + font-size: 17px; + line-height: 1.35; + margin: 0; + font-weight: 700; + letter-spacing: -0.01em; + color: var(--cpw-ink-1); + } + + .cpw-desc { + margin: 0; + color: var(--cpw-muted); + font-size: 14.5px; + line-height: 1.55; + flex: 1; + } + + /* Meta + CTA */ + .cpw-meta { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + margin-top: 8px; + padding-top: 14px; + border-top: 1px dashed rgba(15, 23, 42, 0.1); + } + + .cpw-cta { + display: inline-flex; + align-items: center; + gap: 8px; + color: var(--cpw-primary-1); + font-weight: 700; + font-size: 13.5px; + letter-spacing: 0.02em; + user-select: none; + transition: gap 0.25s ease, color 0.25s ease; + + svg { + width: 14px; + height: 14px; + transition: transform 0.25s ease; + } + } + + &:not(.editor_enable) .cpw-card:hover .cpw-cta { + gap: 12px; + color: var(--cpw-primary-2); + + svg { + transform: translate(2px, -2px); + } + } + + /* Small "verified" icon in meta */ + .cpw-verified { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--cpw-muted); + font-size: 12px; + font-weight: 600; + + svg { + width: 14px; + height: 14px; + color: var(--cpw-primary-1); + } + } +} diff --git a/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.js b/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.js new file mode 100644 index 000000000..2d8971c85 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.js @@ -0,0 +1,70 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.DeltatechSpecialProducts = publicWidget.Widget.extend({ + selector: ".s_deltatech_special_products", + events: { + "click .s_deltatech_special_product_toggle": "_onToggleDetails", + }, + + /** + * @override + */ + start() { + if (!this.el) return this._super.apply(this, arguments); + // In editor mode, details are always visible via CSS, but we ensure we don't interfere + if (this.el.closest("body").classList.contains("editor_enable")) { + return this._super.apply(this, arguments); + } + this.scrollTimeout = null; + return this._super.apply(this, arguments); + }, + + destroy() { + if (this.scrollTimeout) { + clearTimeout(this.scrollTimeout); + } + this._super.apply(this, arguments); + }, + + /** + * @private + * @param {Event} ev + */ + _onToggleDetails(ev) { + const $btn = $(ev.currentTarget); + const $item = $btn.closest(".s_deltatech_special_product_item"); + const $details = $item.find(".s_deltatech_special_product_details"); + const isActive = $item.hasClass("is-active"); + + // Close all other items + const $others = this.$(".s_deltatech_special_product_item").not($item); + $others.removeClass("is-active"); + $others.find(".s_deltatech_special_product_details").addClass("d-none"); + this.$(".s_deltatech_special_product_toggle").not($btn).text("View Details"); + + // Toggle current item + $item.toggleClass("is-active"); + $details.toggleClass("d-none"); + $btn.text(!isActive ? "Hide Details" : "View Details"); + + if (!isActive) { + // Scroll to the item if it's not fully in view after expansion + this.scrollTimeout = setTimeout(() => { + if (!this.el || !document.contains(this.el)) return; + const headerOffset = 100; // Account for potential sticky header + const elementPosition = $item[0].getBoundingClientRect().top; + const offsetPosition = elementPosition + window.pageYOffset - headerOffset; + + window.scrollTo({ + top: offsetPosition, + behavior: "smooth", + }); + this.scrollTimeout = null; + }, 500); // Increased delay for mobile responsiveness + } + }, +}); + +export default publicWidget.registry.DeltatechSpecialProducts; diff --git a/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.scss b/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.scss new file mode 100644 index 000000000..2eb7e8986 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_deltatech_special_products/000.scss @@ -0,0 +1,136 @@ +.s_deltatech_special_products { + .s_deltatech_special_products_grid { + display: flex; + flex-wrap: wrap; + } + + .s_deltatech_special_product_item { + transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1); + &.is-active { + flex: 0 0 100% !important; + max-width: 100% !important; + + .card { + flex-direction: row; + flex-wrap: wrap; + align-items: flex-start; + + .card-img-top { + flex: 0 0 300px; + height: 100% !important; + min-height: 250px; + + @media (max-width: 767.98px) { + flex: 0 0 100%; + height: auto !important; + min-height: unset; + } + } + + .card-body { + flex: 1; + text-align: left !important; + display: flex; + flex-direction: column; + justify-content: flex-start; + padding: 2rem !important; + flex-wrap: wrap; + + @media (max-width: 767.98px) { + flex: 0 0 100%; + padding: 1.5rem !important; + } + + .card-title, + .card-text, + .s_deltatech_special_product_toggle { + width: 100%; + } + } + } + } + } + + .card { + transition: transform 0.3s ease, box-shadow 0.3s ease; + &:hover { + transform: translateY(-5px); + box-shadow: 0 10px 20px rgba(0, 0, 0, 0.1) !important; + } + } + + .s_deltatech_special_product_details { + &.d-none { + display: none !important; + } + background: #fdfdfd; + width: 100%; + margin-top: 1.5rem; + border-radius: 8px; + box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.05); + animation: fadeIn 0.5s ease; + + @media (max-width: 767.98px) { + padding: 1rem !important; + + .row > [class*="col-"] { + flex: 0 0 100%; + max-width: 100%; + margin-bottom: 1.5rem; + + &:last-child { + margin-bottom: 0; + } + } + } + + h5 { + font-size: 1.1rem; + font-weight: 600; + color: #333; + margin-bottom: 1rem; + } + + ul li { + margin-bottom: 0.5rem; + font-size: 0.95rem; + color: #555; + } + + .s_deltatech_special_product_description { + font-size: 0.95rem; + line-height: 1.6; + color: #666; + } + } + + /* When an item is active, its details should be visible and full width */ + .s_deltatech_special_product_item.is-active { + .s_deltatech_special_product_details { + display: block !important; + } + } + + .s_deltatech_special_product_toggle { + transition: all 0.3s ease; + } +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +body.editor_enable .s_deltatech_special_products { + .s_deltatech_special_product_details { + display: block !important; + width: 100%; + } + .s_deltatech_special_product_toggle { + display: none !important; + } +} diff --git a/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.js b/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.js new file mode 100644 index 000000000..7c1922a25 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.js @@ -0,0 +1,141 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.PartnersLoop = publicWidget.Widget.extend({ + selector: ".js-partners-loop", + events: { + "click .partners-nav.prev": "_onPrevClick", + "click .partners-nav.next": "_onNextClick", + }, + + /** + * @override + */ + start: function () { + this.currentPosition = 0; + this.initPartnersLoop(); + return this._super.apply(this, arguments); + }, + + initPartnersLoop: function () { + const section = this.el; + if (!section) return; + const container = section.querySelector(".partners-container"); + const track = section.querySelector(".partners-track"); + if (!track || !container) return; + + // Check if we are in edit mode + const isEditMode = + !!document.querySelector("#oe_snippets") || + document.body.classList.contains("editor_enable") || + section.closest(".o_editable_auto_status") || + !!section.closest(".o_we_website_top_menu"); + + // Always clean up first + section.classList.remove("is-ready"); + track.querySelectorAll('[aria-hidden="true"]').forEach((el) => el.remove()); + delete track.dataset.duplicated; + track.style.removeProperty("animation"); + track.style.removeProperty("transform"); + + if (isEditMode) { + // In edit mode, we want to make sure no clones exist and no animation is running. + // We already did the cleanup above. + // Additionally, we want to make sure the track doesn't have clones even if they were just added + // by some other instance or saved by mistake. + return; + } + + const items = Array.from(track.children); + if (items.length === 0) return; + + // We use a small timeout to ensure layout is calculated, especially for the width + this.initTimeout = setTimeout(() => { + if (!this.el) return; + const container = this.el.querySelector(".partners-container"); + const track = this.el.querySelector(".partners-track"); + if (!container || !track) return; + const containerWidth = container.offsetWidth; + + // Loop Mode + items.forEach(function (item) { + const clone = item.cloneNode(true); + clone.setAttribute("aria-hidden", "true"); + track.appendChild(clone); + }); + track.dataset.duplicated = "true"; + + // Force a reflow to ensure the browser has updated the scrollWidth + const trackWidth = track.scrollWidth; + + requestAnimationFrame(() => { + section.classList.add("is-ready"); + + // If items fit in the container (after duplication), check if we still want animation + if (trackWidth <= containerWidth) { + track.style.setProperty("animation", "none", "important"); + } + }); + }, 100); + }, + + destroy: function () { + if (this.initTimeout) { + clearTimeout(this.initTimeout); + } + this._super.apply(this, arguments); + }, + + _updateNavButtons: function () { + const container = this.el.querySelector(".partners-container"); + const track = this.el.querySelector(".partners-track"); + const prevBtn = this.el.querySelector(".partners-nav.prev"); + const nextBtn = this.el.querySelector(".partners-nav.next"); + + if (!container || !track || !prevBtn || !nextBtn) return; + + const containerWidth = container.offsetWidth; + const trackWidth = track.scrollWidth; + + if (trackWidth > containerWidth) { + prevBtn.classList.remove("d-none"); + nextBtn.classList.remove("d-none"); + } else { + prevBtn.classList.add("d-none"); + nextBtn.classList.add("d-none"); + } + }, + + _onPrevClick: function (ev) { + if (document.body.classList.contains("editor_enable")) return; + ev.preventDefault(); + const container = this.el.querySelector(".partners-container"); + const track = this.el.querySelector(".partners-track"); + const step = 220; // Width of partner-item + + this.currentPosition += step; + if (this.currentPosition > 0) { + this.currentPosition = 0; + } + track.style.setProperty("transform", `translateX(${this.currentPosition}px)`, "important"); + }, + + _onNextClick: function (ev) { + if (document.body.classList.contains("editor_enable")) return; + ev.preventDefault(); + const container = this.el.querySelector(".partners-container"); + const track = this.el.querySelector(".partners-track"); + const step = 220; + const containerWidth = container.offsetWidth; + const trackWidth = track.scrollWidth; + + this.currentPosition -= step; + if (Math.abs(this.currentPosition) > trackWidth - containerWidth) { + this.currentPosition = -(trackWidth - containerWidth); + } + track.style.setProperty("transform", `translateX(${this.currentPosition}px)`, "important"); + }, +}); + +export default publicWidget.registry.PartnersLoop; diff --git a/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.scss b/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.scss new file mode 100644 index 000000000..84e146da4 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_partners_loop/000.scss @@ -0,0 +1,177 @@ +.s_deltatech_partners_loop { + --partners-bg: #fff; + --partners-title-color: inherit; + --partners-nav-bg: rgba(0, 0, 0, 0.2); + --partners-nav-hover-bg: rgba(0, 0, 0, 0.5); + + text-align: center; + padding: 40px 0; + overflow: hidden; + background: var(--partners-bg); + + &.is-ready .partners-track { + animation: partnersMove 38s linear infinite; + } +} + +body.editor_enable .s_deltatech_partners_loop, +.o_editable_auto_status .s_deltatech_partners_loop { + .partners-track { + animation: none !important; + transform: none !important; + flex-wrap: wrap; + width: 100% !important; + justify-content: center; + } + .partners-nav { + display: none !important; + } +} + +.partners-title { + font-size: 32px; + font-weight: 300; + margin-bottom: 40px; + color: var(--partners-title-color); +} + +.partners-container { + width: 100%; + overflow: hidden; + position: relative; + display: flex; + align-items: center; + + &::before, + &::after { + content: ""; + position: absolute; + top: 0; + width: 120px; + height: 100%; + z-index: 2; + pointer-events: none; + } + + &::before { + left: 0; + background: linear-gradient(to right, var(--partners-bg) 0%, rgba(255, 255, 255, 0) 100%); + } + + &::after { + right: 0; + background: linear-gradient(to left, var(--partners-bg) 0%, rgba(255, 255, 255, 0) 100%); + } + + &:hover .partners-nav { + opacity: 1; + } +} + +.partners-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + width: 40px; + height: 40px; + background: var(--partners-nav-bg); + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + z-index: 10; + transition: all 0.3s ease; + color: #fff; + border: none; + opacity: 0; + + &:hover { + background: var(--partners-nav-hover-bg); + color: #fff; + } + + &.prev { + left: 10px; + } + + &.next { + right: 10px; + } + + &.d-none { + display: none !important; + } +} + +.partners-track { + display: flex; + width: max-content; + align-items: center; + transition: transform 0.5s ease; + + &:hover .partner-logo img { + filter: grayscale(100%); + opacity: 0.5; + } + + .partner-item:hover img { + filter: grayscale(0%); + opacity: 1; + transform: scale(1.08); + } +} + +.partner-item { + width: 220px; + flex: 0 0 220px; + padding: 10px; + display: flex; + align-items: center; + justify-content: center; + transition: transform 0.3s ease; +} + +.partner-logo { + height: 90px; + display: flex; + align-items: center; + justify-content: center; + + img { + max-width: 100%; + max-height: 100%; + object-fit: contain; + transition: all 0.3s ease; + } +} + +@keyframes partnersMove { + from { + transform: translate3d(0, 0, 0); + } + + to { + transform: translate3d(-50%, 0, 0); + } +} + +@media (max-width: 768px) { + .partner-item { + width: 170px; + flex: 0 0 170px; + } + + .partner-logo { + height: 70px; + } + + .partners-title { + font-size: 26px; + } + + .partners-container::before, + .partners-container::after { + width: 60px; + } +} diff --git a/deltatech_website_blocks/static/src/snippets/s_partners_loop/options.js b/deltatech_website_blocks/static/src/snippets/s_partners_loop/options.js new file mode 100644 index 000000000..8dcb8d84b --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_partners_loop/options.js @@ -0,0 +1,32 @@ +/** @odoo-module **/ + +import options from "@web_editor/js/editor/snippets.options"; + +options.registry.PartnersLoopOptions = options.Class.extend({ + + async cleanForSave() { + await this._super(...arguments); + this.$target.find('.partners-track [aria-hidden="true"]').remove(); + const track = this.$target.find(".partners-track")[0]; + if (track) { + delete track.dataset.duplicated; + track.style.removeProperty("transform"); + track.style.removeProperty("animation"); + } + this.$target[0].classList.remove("is-ready"); + }, + + onBuilt() { + this._super(...arguments); + this.$target.find('.partners-track [aria-hidden="true"]').remove(); + const track = this.$target.find(".partners-track")[0]; + if (track) { + delete track.dataset.duplicated; + track.style.removeProperty("transform"); + track.style.removeProperty("animation"); + } + this.$target[0].classList.remove("is-ready"); + }, +}); + +export default options.registry.PartnersLoopOptions; \ No newline at end of file diff --git a/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.js b/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.js new file mode 100644 index 000000000..285e9bf2c --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.js @@ -0,0 +1,182 @@ +/** @odoo-module **/ + +import publicWidget from "@web/legacy/js/public/public_widget"; + +publicWidget.registry.StatsCounters = publicWidget.Widget.extend({ + selector: ".s_deltatech_stats_counters", + + /** + * @override + */ + start: function () { + this.hasRun = false; + this.GLOBAL_DURATION_MS = 5200; + this.LOOP_PAUSE_MS = 900; + this.FADE_OUT_MS = 450; + this.RESTART_DELAY_MS = 250; + this.timeouts = []; + + this.initAnimation(); + return this._super.apply(this, arguments); + }, + + destroy: function () { + if (this.observer) { + this.observer.disconnect(); + } + this.timeouts.forEach(clearTimeout); + this._super.apply(this, arguments); + }, + + initAnimation: function () { + if (!this.el) return; + if (this.el.closest(".editor_enable") || this.el.closest(".o_editable_auto_status")) { + // In editor mode, set counters to their target values without animation + this.setCountersToTarget(); + return; + } + + this.observer = new IntersectionObserver( + (entries) => { + for (const entry of entries) { + if (entry.isIntersecting) { + this.startAnimationLoop(); + this.observer.disconnect(); + this.observer = null; + } + } + }, + { + threshold: 0.05, + rootMargin: "0px 0px -5% 0px", + } + ); + this.observer.observe(this.el); + }, + + setCountersToTarget: function () { + this.el.querySelectorAll(".count").forEach((el) => { + const target = Number(el.dataset.target || 0); + const mode = el.dataset.format || "plain"; + el.textContent = this.formatNumber(target, mode); + }); + }, + + formatNumber: function (n, mode) { + if (mode === "group") { + return Math.round(n).toLocaleString("en-US"); + } + return String(Math.round(n)); + }, + + startAnimationLoop: function () { + if (this.hasRun) return; + this.hasRun = true; + + const counters = Array.from(this.el.querySelectorAll(".count")).map((el) => ({ + el: el, + valueEl: el.closest(".value"), + target: Number(el.dataset.target || 0), + mode: el.dataset.format || "plain", + })); + + const easeOutCubic = (t) => 1 - Math.pow(1 - t, 3); + + const setAllTo = (multiplier) => { + for (const c of counters) { + c.el.textContent = this.formatNumber(c.target * multiplier, c.mode); + } + }; + + const fadeOutAll = () => { + for (const c of counters) { + if (c.valueEl) { + c.valueEl.classList.add("is-fading-out"); + } + } + }; + + const fadeInAll = () => { + for (const c of counters) { + if (c.valueEl) { + c.valueEl.classList.remove("is-fading-out"); + } + } + }; + + const runCycle = () => { + // Check if we are still in the DOM and NOT in editor mode + if (!this.el || this.el.closest(".editor_enable") || this.el.closest(".o_editable_auto_status")) return; + + fadeInAll(); + setAllTo(0); + + const start = performance.now(); + + const frame = (now) => { + if (!this.el || this.el.closest(".editor_enable") || this.el.closest(".o_editable_auto_status")) return; + + const t = Math.min(1, (now - start) / this.GLOBAL_DURATION_MS); + const k = easeOutCubic(t); + + for (const c of counters) { + c.el.textContent = this.formatNumber(c.target * k, c.mode); + } + + if (t < 1) { + requestAnimationFrame(frame); + } else { + for (const c of counters) { + c.el.textContent = this.formatNumber(c.target, c.mode); + } + + this.timeouts.push( + setTimeout(() => { + if ( + !this.el || + this.el.closest(".editor_enable") || + this.el.closest(".o_editable_auto_status") + ) + return; + fadeOutAll(); + + this.timeouts.push( + setTimeout(() => { + if ( + !this.el || + this.el.closest(".editor_enable") || + this.el.closest(".o_editable_auto_status") + ) + return; + setAllTo(0); + + requestAnimationFrame(() => { + if ( + !this.el || + this.el.closest(".editor_enable") || + this.el.closest(".o_editable_auto_status") + ) + return; + fadeInAll(); + + this.timeouts.push( + setTimeout(() => { + runCycle(); + }, this.RESTART_DELAY_MS) + ); + }); + }, this.FADE_OUT_MS) + ); + }, this.LOOP_PAUSE_MS) + ); + } + }; + + requestAnimationFrame(frame); + }; + + runCycle(); + }, +}); + +export default publicWidget.registry.StatsCounters; diff --git a/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.scss b/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.scss new file mode 100644 index 000000000..350e59888 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_stats_counters/000.scss @@ -0,0 +1,84 @@ +.s_deltatech_stats_counters { + --stat-primary: #007bff; + --text: #222; + --muted: #666; + + background: radial-gradient(1200px 400px at 50% 20%, rgba(0, 0, 0, 0.03), transparent 60%), + linear-gradient(#ffffff, #ffffff); + padding: 38px 16px 44px; + + .stats-container { + max-width: 1100px; + margin: 0 auto; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 18px; + } + + .stat { + text-align: center; + padding: 8px 8px 10px; + position: relative; + } + + .value { + color: var(--stat-primary); + font-weight: 800; + letter-spacing: 0.5px; + font-size: 44px; + line-height: 1.05; + margin-bottom: 10px; + display: flex; + justify-content: center; + align-items: baseline; + gap: 2px; + white-space: nowrap; + opacity: 1; + transform: translateY(0); + transition: opacity 450ms ease, transform 450ms ease; + + &.is-fading-out { + opacity: 0; + transform: translateY(-6px); + } + } + + .suffix { + font-size: 34px; + font-weight: 800; + color: var(--stat-primary); + transform: translateY(-2px); + } + + .label { + color: var(--muted); + font-size: 13px; + line-height: 1.25; + max-width: 260px; + margin: 0 auto; + } +} + +@media (max-width: 900px) { + .s_deltatech_stats_counters .stats-container { + justify-content: center; + } +} + +@media (max-width: 480px) { + .s_deltatech_stats_counters { + .stats-container { + flex-direction: column; + align-items: center; + } + + .value { + font-size: 40px; + } + + .suffix { + font-size: 30px; + } + } +} diff --git a/deltatech_website_blocks/static/src/snippets/s_stats_counters/options.js b/deltatech_website_blocks/static/src/snippets/s_stats_counters/options.js new file mode 100644 index 000000000..cbee90560 --- /dev/null +++ b/deltatech_website_blocks/static/src/snippets/s_stats_counters/options.js @@ -0,0 +1,43 @@ +/** @odoo-module **/ + +import options from "@web_editor/js/editor/snippets.options"; + +options.registry.StatsCountersOptions = options.Class.extend({ + + onBuilt() { + this._super(...arguments); + this._updateCounters(); + }, + + async onTargetShow() { + await this._super(...arguments); + this._updateCounters(); + }, + + async cleanForSave() { + await this._super(...arguments); + this._updateCounters(); + }, + + // Called when "Target Value" or "Format" widgets change (data-select-data-attribute). + async selectDataAttribute(previewMode, widgetValue, params) { + await this._super(...arguments); + if (params.attributeName === "target" || params.attributeName === "format") { + this._updateCounters(); + } + }, + + // Mirrors StatsCounters.setCountersToTarget() from 000.js. + _updateCounters() { + const section = this.$target[0].closest(".s_deltatech_stats_counters") || this.$target[0]; + section.querySelectorAll(".count").forEach((el) => { + const target = Number(el.dataset.target || 0); + const mode = el.dataset.format || "plain"; + el.textContent = mode === "group" + ? Math.round(target).toLocaleString("en-US") + : String(Math.round(target)); + }); + }, +}); + +export default options.registry.StatsCountersOptions; \ No newline at end of file diff --git a/deltatech_website_blocks/views/snippets/s_cards_grid.xml b/deltatech_website_blocks/views/snippets/s_cards_grid.xml new file mode 100644 index 000000000..2c7dd46b2 --- /dev/null +++ b/deltatech_website_blocks/views/snippets/s_cards_grid.xml @@ -0,0 +1,131 @@ + + + + diff --git a/deltatech_website_blocks/views/snippets/s_deltatech_special_products.xml b/deltatech_website_blocks/views/snippets/s_deltatech_special_products.xml new file mode 100644 index 000000000..0768ab6ab --- /dev/null +++ b/deltatech_website_blocks/views/snippets/s_deltatech_special_products.xml @@ -0,0 +1,128 @@ + + + + diff --git a/deltatech_website_blocks/views/snippets/s_partners_loop.xml b/deltatech_website_blocks/views/snippets/s_partners_loop.xml new file mode 100644 index 000000000..bcf970a9f --- /dev/null +++ b/deltatech_website_blocks/views/snippets/s_partners_loop.xml @@ -0,0 +1,90 @@ + + + + diff --git a/deltatech_website_blocks/views/snippets/s_stats_counters.xml b/deltatech_website_blocks/views/snippets/s_stats_counters.xml new file mode 100644 index 000000000..7bc1a08c2 --- /dev/null +++ b/deltatech_website_blocks/views/snippets/s_stats_counters.xml @@ -0,0 +1,58 @@ + + + + diff --git a/deltatech_website_blocks/views/snippets/snippets.xml b/deltatech_website_blocks/views/snippets/snippets.xml new file mode 100644 index 000000000..a81f6d9ef --- /dev/null +++ b/deltatech_website_blocks/views/snippets/snippets.xml @@ -0,0 +1,218 @@ + + + +