Skip to content

Commit 99fc630

Browse files
committed
[ADD] website_product_quotation: add a new page in website
Add a new interactive page 'Product Quote' which shows predefined products. The goal of this commit was to create an interactive menu by implementing interaction framework to a bootstrap website.
1 parent 6403263 commit 99fc630

File tree

10 files changed

+1059
-0
lines changed

10 files changed

+1059
-0
lines changed

website_product_quotation/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import controllers
+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"name": "Website Product Quotation",
3+
'category': 'Website/Website',
4+
"description": "A website page to display set of products",
5+
"author": "Darpan",
6+
"depends": ["website","website_sale","crm"],
7+
"application": True,
8+
"installable": True,
9+
"auto_install": ["website"],
10+
"data": ["views/product_quote_template.xml"],
11+
"assets": {
12+
"web.assets_frontend":[
13+
"website_product_quotation/static/src/scss/product_quote_template.scss",
14+
"website_product_quotation/static/src/interactions/product_quote.js"
15+
]
16+
},
17+
"license": "AGPL-3"
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from . import product_quotation_controller
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
from odoo import http
2+
from odoo.http import Controller, request
3+
4+
class ProductQuotationController(Controller):
5+
6+
@http.route("/product_quotation", type="http", auth="public", website=True)
7+
def show_page(self):
8+
product_names = ["Product A", "Product B", "Product C", "Product D", "Product E", "Product F", "Product G", "Product H"]
9+
10+
products = request.env['product.template'].sudo().search([
11+
('name', 'in', product_names)
12+
], limit=8)
13+
14+
product_data = []
15+
website = request.website
16+
17+
for product in products:
18+
extra_images = request.env['product.image'].sudo().search([
19+
('product_tmpl_id', '=', product.id)
20+
])
21+
22+
extra_image_data = [
23+
{
24+
"url": website.image_url(img, "image_1024"),
25+
"name": img.name if img.name else "Untitled Image",
26+
}
27+
for img in extra_images
28+
]
29+
30+
product_data.append({
31+
'id': product.id,
32+
'name': product.name,
33+
'image_url': website.image_url(product, "image_1024"),
34+
'extra_images': extra_image_data,
35+
'size': product.attribute_line_ids.filtered(lambda l: l.attribute_id.name == 'Size').mapped('value_ids.name'),
36+
'color': product.attribute_line_ids.filtered(lambda l: l.attribute_id.name == 'Color').mapped('value_ids.name'),
37+
'fabric': product.attribute_line_ids.filtered(lambda l: l.attribute_id.name == 'Fabric').mapped('value_ids.name'),
38+
'print': product.attribute_line_ids.filtered(lambda l: l.attribute_id.name == 'Print').mapped('value_ids.name'),
39+
'url': f"/shop/product/{product.id}",
40+
})
41+
42+
return request.render("website_product_quotation.product_quote_template", {
43+
'products': product_data
44+
})
Loading
Loading
Loading
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,222 @@
1+
import { Interaction } from "@web/public/interaction";
2+
import { registry } from "@web/core/registry";
3+
4+
export class ProductQuote extends Interaction {
5+
static selector = ".o_product_quote";
6+
7+
dynamicContent = {
8+
".o_next": {
9+
"t-on-click": () => {
10+
this.rotateToIndex(this.currentIndex + 1);
11+
},
12+
},
13+
".o_prev": {
14+
"t-on-click": () => {
15+
this.rotateToIndex(this.currentIndex - 1);
16+
},
17+
},
18+
".thumb-item": {
19+
"t-on-click": (event) => {
20+
this.imageUpdater(event);
21+
},
22+
},
23+
".o_copy_link": {
24+
"t-on-click": (event) => {
25+
this.copyShareLink(event);
26+
},
27+
},
28+
".o_get_quote_btn": {
29+
"t-on-click": () => {
30+
this.isShown = false;
31+
this.updateFormDetails();
32+
},
33+
},
34+
".o_form_section": {
35+
"t-att-class": () => ({ "d-none": this.isShown }),
36+
"t-on-click": (ev) => {
37+
console.log("event captured");
38+
39+
const form_container =
40+
this.el.querySelector(".o_form_container");
41+
if (form_container && !form_container.contains(ev.target)) {
42+
console.log("updated");
43+
44+
this.isShown = true;
45+
}
46+
},
47+
},
48+
};
49+
50+
setup() {
51+
console.log(this.el);
52+
console.log("ProductQuote Interaction initialized");
53+
this.ul = this.el.querySelector("#circle--rotate");
54+
this.lis = this.ul.querySelectorAll("li");
55+
this.totalItems = this.lis.length;
56+
this.step = 360 / this.totalItems;
57+
this.animates = this.el.querySelectorAll(".animate");
58+
this.currentIndex = 0;
59+
this.angle = 0;
60+
this.isShown = true;
61+
this.positionListItems();
62+
}
63+
64+
positionListItems() {
65+
const center = {
66+
x: this.ul.clientWidth / 2,
67+
y: this.ul.clientHeight / 2,
68+
};
69+
const radius =
70+
(Math.min(this.ul.clientWidth, this.ul.clientHeight) / 2) * 0.82;
71+
const emBase = parseFloat(getComputedStyle(this.ul).fontSize);
72+
73+
this.lis.forEach((li, index) => {
74+
const itemAngle =
75+
(index / this.totalItems) * 2 * Math.PI - Math.PI / 2;
76+
const x = (center.x + radius * Math.cos(itemAngle)) / emBase;
77+
const y = (center.y + radius * Math.sin(itemAngle)) / emBase;
78+
79+
li.style.left = `${x}em`;
80+
li.style.top = `${y}em`;
81+
82+
const degrees = (itemAngle * 180) / Math.PI + 90;
83+
li.style.transform = `translate(-50%, -50%) rotate(${degrees}deg)`;
84+
85+
const icon = li.querySelector(".icon");
86+
if (icon) {
87+
icon.style.transform = `rotate(0deg)`;
88+
}
89+
90+
li.addEventListener("click", () => this.rotateToIndex(index));
91+
});
92+
}
93+
94+
rotateToIndex(index) {
95+
if (index < 0) index = this.totalItems - 1;
96+
if (index >= this.totalItems) index = 0;
97+
98+
this.currentIndex = index;
99+
this.angle = -index * this.step;
100+
this.ul.style.transform = `rotate(${this.angle}deg)`;
101+
this.lis.forEach((li, i) => li.classList.toggle("active", i === index));
102+
this.animates.forEach((animate, i) => {
103+
if (i === index) {
104+
animate.classList.add("active");
105+
} else {
106+
animate.classList.remove("active");
107+
}
108+
});
109+
}
110+
111+
imageUpdater(event) {
112+
const element = event.target;
113+
let parentContainer = element.closest(".product-category-slider");
114+
115+
let mainImage = parentContainer.querySelector(".img-fluid");
116+
let mainImageName = parentContainer.querySelector(".extra-img-name");
117+
let activeImages = parentContainer.querySelectorAll(".thumb-item");
118+
119+
if (mainImage) {
120+
mainImage.src = element.getAttribute("src");
121+
}
122+
123+
if (mainImageName) {
124+
mainImageName.textContent = element.getAttribute("data-name") || "";
125+
}
126+
127+
activeImages.forEach((img) => img.classList.remove("activeimg"));
128+
element.classList.add("activeimg");
129+
}
130+
131+
copyShareLink(event) {
132+
event.preventDefault();
133+
const element = event.target;
134+
const link = element.href;
135+
navigator.clipboard.writeText(link);
136+
alert("Copied the text: \n" + link);
137+
}
138+
139+
updateFormDetails() {
140+
this.active_animate_wrapper = this.el.querySelectorAll(".active")[1];
141+
this.product = this.active_animate_wrapper.getAttribute("data-key");
142+
this.product = JSON.parse(this.product.replace(/'/g, '"'));
143+
144+
this.product_name =
145+
this.active_animate_wrapper.querySelector(
146+
".o_product_name"
147+
).textContent;
148+
this.el.querySelector("#product_name").value = this.product.name || "";
149+
150+
this.populateOptions("Size", this.product.size || []);
151+
this.populateOptions("Fabric", this.product.fabric || []);
152+
this.populateOptions("Color", this.product.color || []);
153+
this.populateOptions("Print", this.product.print || []);
154+
}
155+
156+
populateOptions(fieldName, options) {
157+
const formContainer = this.el.querySelector("form .row");
158+
if (!formContainer) return;
159+
160+
const submitButtonContainer = formContainer.querySelector(
161+
".s_website_form_submit"
162+
);
163+
164+
if (!options.length) {
165+
const existingBlock = formContainer.querySelector(
166+
`.s_website_form_field[data-name='${fieldName}']`
167+
);
168+
if (existingBlock) existingBlock.remove();
169+
return;
170+
}
171+
172+
let fieldBlock = formContainer.querySelector(
173+
`.s_website_form_field[data-name='${fieldName}']`
174+
);
175+
176+
if (!fieldBlock) {
177+
fieldBlock = document.createElement("div");
178+
fieldBlock.className =
179+
"s_website_form_field mb-3 col-12 s_website_form_custom col-lg-11";
180+
fieldBlock.setAttribute("data-name", fieldName);
181+
fieldBlock.setAttribute("data-type", "one2many");
182+
183+
fieldBlock.innerHTML = `
184+
<label class="s_website_form_label">
185+
<span class="s_website_form_label_content">${fieldName}</span>
186+
</label>
187+
<div class="row s_col_no_resize s_col_no_bgcolor s_website_form_multiple" data-name="${fieldName}" data-display="horizontal">
188+
</div>
189+
`;
190+
191+
formContainer.insertBefore(fieldBlock, submitButtonContainer);
192+
}
193+
194+
const container = fieldBlock.querySelector(`.s_website_form_multiple`);
195+
container.innerHTML = "";
196+
197+
const fragment = document.createDocumentFragment();
198+
options.forEach((option, index) => {
199+
const newCheckbox = document.createElement("div");
200+
newCheckbox.className = "checkbox col-12 col-lg-4 col-md-6";
201+
newCheckbox.innerHTML = `
202+
<div class="form-check">
203+
<input type="checkbox" class="s_website_form_input form-check-input"
204+
id="${fieldName.toLowerCase()}_option_${index}"
205+
name="${fieldName}"
206+
value="${option}" />
207+
<label class="form-check-label s_website_form_check_label"
208+
for="${fieldName.toLowerCase()}_option_${index}">
209+
${option}
210+
</label>
211+
</div>
212+
`;
213+
fragment.appendChild(newCheckbox);
214+
});
215+
216+
container.appendChild(fragment);
217+
}
218+
}
219+
220+
registry
221+
.category("public.interactions")
222+
.add("website_product_quotation.product_quote", ProductQuote);

0 commit comments

Comments
 (0)