665 lines
21 KiB
JavaScript
665 lines
21 KiB
JavaScript
import React, { Component } from "react";
|
|
import Button from "@mui/material/Button";
|
|
import ButtonGroup from "@mui/material/ButtonGroup";
|
|
import IconButton from "@mui/material/IconButton";
|
|
import Typography from "@mui/material/Typography";
|
|
import Box from "@mui/material/Box";
|
|
import Tooltip from "@mui/material/Tooltip";
|
|
import TextField from "@mui/material/TextField";
|
|
import AddIcon from "@mui/icons-material/Add";
|
|
import RemoveIcon from "@mui/icons-material/Remove";
|
|
import ShoppingCartIcon from "@mui/icons-material/ShoppingCart";
|
|
import DeleteIcon from "@mui/icons-material/Delete";
|
|
import NotificationsIcon from "@mui/icons-material/Notifications";
|
|
import NotificationsActiveIcon from "@mui/icons-material/NotificationsActive";
|
|
import CircularProgress from "@mui/material/CircularProgress";
|
|
import { withI18n } from "../i18n/withTranslation.js";
|
|
import {
|
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
|
emitPushSubscriptionsChanged,
|
|
isPushApiSupported,
|
|
fetchPushConfiguration,
|
|
registerPushServiceWorker,
|
|
ensurePushSubscription,
|
|
articlePushStatus,
|
|
articlePushSubscribe,
|
|
articlePushUnsubscribe,
|
|
parseSubscribedStatus,
|
|
parseSuccess,
|
|
} from "../utils/articlePush.js";
|
|
|
|
if (!Array.isArray(window.cart)) window.cart = [];
|
|
|
|
class AddToCartButton extends Component {
|
|
constructor(props) {
|
|
super(props);
|
|
if (!Array.isArray(window.cart)) window.cart = [];
|
|
this.state = {
|
|
quantity: window.cart.find((i) => i.id === this.props.id)
|
|
? window.cart.find((i) => i.id === this.props.id).quantity
|
|
: 0,
|
|
isEditing: false,
|
|
editValue: "",
|
|
pushInteractive: false,
|
|
pushSubscribed: false,
|
|
pushBusy: false,
|
|
pushError: null,
|
|
};
|
|
}
|
|
|
|
kArtikelNumber = () => {
|
|
const { id } = this.props;
|
|
const n = typeof id === "number" ? id : parseInt(id, 10);
|
|
return Number.isFinite(n) && n > 0 ? n : null;
|
|
};
|
|
|
|
refreshIncomingPushStatus = async () => {
|
|
const { available, incoming } = this.props;
|
|
if (available || !incoming) {
|
|
this.setState({
|
|
pushInteractive: false,
|
|
pushSubscribed: false,
|
|
pushError: null,
|
|
});
|
|
return;
|
|
}
|
|
const kArtikel = this.kArtikelNumber();
|
|
if (!kArtikel || !isPushApiSupported()) {
|
|
this.setState({ pushInteractive: false });
|
|
return;
|
|
}
|
|
try {
|
|
const cfg = await fetchPushConfiguration();
|
|
if (!cfg.configured || !cfg.publicKey) {
|
|
this.setState({ pushInteractive: false });
|
|
return;
|
|
}
|
|
await registerPushServiceWorker();
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const subscription = await registration.pushManager.getSubscription();
|
|
if (!subscription) {
|
|
this.setState({
|
|
pushInteractive: true,
|
|
pushSubscribed: false,
|
|
pushError: null,
|
|
});
|
|
return;
|
|
}
|
|
const statusData = await articlePushStatus(kArtikel, subscription.endpoint);
|
|
this.setState({
|
|
pushInteractive: true,
|
|
pushSubscribed: parseSubscribedStatus(statusData),
|
|
pushError: null,
|
|
});
|
|
} catch (e) {
|
|
console.warn("AddToCartButton: incoming push init failed", e);
|
|
this.setState({ pushInteractive: false });
|
|
}
|
|
};
|
|
|
|
handleIncomingPushClick = async () => {
|
|
if (!this.state.pushInteractive || this.state.pushBusy) return;
|
|
const kArtikel = this.kArtikelNumber();
|
|
if (!kArtikel) return;
|
|
const t = this.props.t;
|
|
this.setState({ pushBusy: true, pushError: null });
|
|
try {
|
|
if (this.state.pushSubscribed) {
|
|
const registration = await navigator.serviceWorker.ready;
|
|
const subscription = await registration.pushManager.getSubscription();
|
|
if (!subscription) {
|
|
this.setState({ pushSubscribed: false, pushBusy: false });
|
|
return;
|
|
}
|
|
const res = await articlePushUnsubscribe(subscription.endpoint, kArtikel);
|
|
if (parseSuccess(res)) {
|
|
this.setState({ pushSubscribed: false });
|
|
emitPushSubscriptionsChanged();
|
|
} else {
|
|
this.setState({
|
|
pushError:
|
|
res?.message ||
|
|
res?.error ||
|
|
(t ? t("productDialogs.pushNotifyError") : ""),
|
|
});
|
|
}
|
|
} else {
|
|
const perm = await Notification.requestPermission();
|
|
if (perm !== "granted") {
|
|
this.setState({
|
|
pushError: t
|
|
? t("productDialogs.pushNotifyPermissionDenied")
|
|
: "",
|
|
pushBusy: false,
|
|
});
|
|
return;
|
|
}
|
|
const cfg = await fetchPushConfiguration();
|
|
if (!cfg.configured || !cfg.publicKey) {
|
|
this.setState({
|
|
pushError: t
|
|
? t("productDialogs.pushNotifyServerDisabled")
|
|
: "",
|
|
pushBusy: false,
|
|
});
|
|
return;
|
|
}
|
|
await registerPushServiceWorker();
|
|
const subscription = await ensurePushSubscription(cfg.publicKey);
|
|
const res = await articlePushSubscribe(kArtikel, subscription);
|
|
if (parseSuccess(res)) {
|
|
this.setState({ pushSubscribed: true });
|
|
emitPushSubscriptionsChanged();
|
|
} else {
|
|
this.setState({
|
|
pushError:
|
|
res?.message ||
|
|
res?.error ||
|
|
(t ? t("productDialogs.pushNotifyError") : ""),
|
|
});
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.error("AddToCartButton: incoming push", e);
|
|
this.setState({
|
|
pushError:
|
|
e.message || (t ? t("productDialogs.pushNotifyError") : ""),
|
|
});
|
|
} finally {
|
|
this.setState({ pushBusy: false });
|
|
}
|
|
};
|
|
|
|
componentDidMount() {
|
|
this.cart = () => {
|
|
if (!Array.isArray(window.cart)) window.cart = [];
|
|
const item = window.cart.find((i) => i.id === this.props.id);
|
|
const newQuantity = item ? item.quantity : 0;
|
|
if (this.state.quantity !== newQuantity)
|
|
this.setState({ quantity: newQuantity });
|
|
};
|
|
this.onPushSubscriptionsChanged = () => {
|
|
this.refreshIncomingPushStatus();
|
|
};
|
|
window.addEventListener("cart", this.cart);
|
|
window.addEventListener(
|
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
|
this.onPushSubscriptionsChanged
|
|
);
|
|
this.refreshIncomingPushStatus();
|
|
}
|
|
|
|
componentDidUpdate(prevProps) {
|
|
if (
|
|
prevProps.available !== this.props.available ||
|
|
prevProps.incoming !== this.props.incoming ||
|
|
prevProps.id !== this.props.id
|
|
) {
|
|
this.refreshIncomingPushStatus();
|
|
}
|
|
}
|
|
|
|
componentWillUnmount() {
|
|
window.removeEventListener("cart", this.cart);
|
|
window.removeEventListener(
|
|
PUSH_SUBSCRIPTIONS_CHANGED_EVENT,
|
|
this.onPushSubscriptionsChanged
|
|
);
|
|
}
|
|
|
|
handleIncrement = () => {
|
|
if (!window.cart) window.cart = [];
|
|
const idx = window.cart.findIndex((item) => item.id === this.props.id);
|
|
if (idx === -1) {
|
|
window.cart.push({
|
|
id: this.props.id,
|
|
name: this.props.name,
|
|
seoName: this.props.seoName,
|
|
pictureList: this.props.pictureList,
|
|
price: this.props.price,
|
|
fGrundPreis: this.props.fGrundPreis,
|
|
cGrundEinheit: this.props.cGrundEinheit,
|
|
quantity: 1,
|
|
weight: this.props.weight,
|
|
vat: this.props.vat,
|
|
versandklasse: this.props.versandklasse,
|
|
availableSupplier: this.props.availableSupplier,
|
|
komponenten: this.props.komponenten,
|
|
available: this.props.available
|
|
});
|
|
} else {
|
|
window.cart[idx].quantity++;
|
|
}
|
|
window.dispatchEvent(new CustomEvent("cart"));
|
|
};
|
|
|
|
handleDecrement = () => {
|
|
if (!window.cart) window.cart = [];
|
|
const idx = window.cart.findIndex((item) => item.id === this.props.id);
|
|
if (idx !== -1) {
|
|
if (window.cart[idx].quantity > 1) {
|
|
window.cart[idx].quantity--;
|
|
} else {
|
|
window.cart.splice(idx, 1);
|
|
}
|
|
window.dispatchEvent(new CustomEvent("cart"));
|
|
}
|
|
};
|
|
|
|
handleClearCart = () => {
|
|
if (!window.cart) window.cart = [];
|
|
const idx = window.cart.findIndex((item) => item.id === this.props.id);
|
|
if (idx !== -1) {
|
|
window.cart.splice(idx, 1);
|
|
window.dispatchEvent(new CustomEvent("cart"));
|
|
}
|
|
};
|
|
|
|
handleEditStart = () => {
|
|
this.setState({
|
|
isEditing: true,
|
|
editValue: this.state.quantity > 0 ? this.state.quantity.toString() : "",
|
|
});
|
|
};
|
|
|
|
handleEditChange = (event) => {
|
|
// Only allow numbers
|
|
const value = event.target.value.replace(/[^0-9]/g, "");
|
|
this.setState({ editValue: value });
|
|
};
|
|
|
|
handleEditComplete = () => {
|
|
let newQuantity = parseInt(this.state.editValue, 10);
|
|
if (isNaN(newQuantity) || newQuantity < 0) {
|
|
newQuantity = 0;
|
|
}
|
|
if (!window.cart) window.cart = [];
|
|
const idx = window.cart.findIndex((item) => item.id === this.props.id);
|
|
if (idx !== -1) {
|
|
window.cart[idx].quantity = newQuantity;
|
|
window.dispatchEvent(
|
|
new CustomEvent("cart", {
|
|
detail: { id: this.props.id, quantity: newQuantity },
|
|
})
|
|
);
|
|
}
|
|
this.setState({ isEditing: false });
|
|
};
|
|
|
|
handleKeyPress = (event) => {
|
|
if (event.key === "Enter") {
|
|
this.handleEditComplete();
|
|
}
|
|
};
|
|
|
|
toggleCart = () => {
|
|
// Dispatch an event that Header.js can listen for to toggle the cart
|
|
window.dispatchEvent(new CustomEvent("toggle-cart"));
|
|
};
|
|
|
|
render() {
|
|
const {
|
|
quantity,
|
|
isEditing,
|
|
editValue,
|
|
pushInteractive,
|
|
pushSubscribed,
|
|
pushBusy,
|
|
pushError,
|
|
} = this.state;
|
|
const { available, size, incoming, availableSupplier } = this.props;
|
|
|
|
// Button is disabled if product is not available
|
|
if (!available) {
|
|
if (incoming) {
|
|
const dateStr = new Date(incoming).toLocaleDateString("de-DE", {
|
|
year: "numeric",
|
|
month: "long",
|
|
day: "numeric",
|
|
});
|
|
const dateLabel = this.props.t
|
|
? this.props.t("cart.availableFrom", { date: dateStr })
|
|
: `Ab ${dateStr}`;
|
|
|
|
return (
|
|
<Box sx={{ width: "100%" }}>
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
size={size || "medium"}
|
|
onClick={pushInteractive ? this.handleIncomingPushClick : undefined}
|
|
disabled={pushInteractive && pushBusy}
|
|
startIcon={
|
|
pushBusy ? (
|
|
<CircularProgress size={18} sx={{ color: "inherit" }} />
|
|
) : pushSubscribed ? (
|
|
<NotificationsActiveIcon sx={{ color: "#2e7d32" }} />
|
|
) : (
|
|
<NotificationsIcon sx={{ color: "rgba(0,0,0,0.75)" }} />
|
|
)
|
|
}
|
|
sx={{
|
|
borderRadius: 2,
|
|
fontWeight: "bold",
|
|
backgroundColor: "#ffeb3b",
|
|
color: "#000000",
|
|
whiteSpace: "nowrap",
|
|
flexWrap: "nowrap",
|
|
"& .MuiButton-label": {
|
|
whiteSpace: "nowrap",
|
|
flexWrap: "nowrap",
|
|
},
|
|
"&:hover": {
|
|
backgroundColor: "#fdd835",
|
|
},
|
|
...(pushInteractive && {
|
|
cursor: "pointer",
|
|
}),
|
|
}}
|
|
>
|
|
{dateLabel}
|
|
</Button>
|
|
{pushError && (
|
|
<Typography
|
|
variant="caption"
|
|
color="error"
|
|
sx={{ display: "block", mt: 0.5, textAlign: "center" }}
|
|
>
|
|
{pushError}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
);
|
|
}
|
|
|
|
// If availableSupplier is 1, handle both quantity cases
|
|
if (availableSupplier === 1) {
|
|
// If no items in cart, show simple "Add to Cart" button with yellowish green
|
|
if (quantity === 0) {
|
|
return (
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
size={size || "medium"}
|
|
onClick={this.handleIncrement}
|
|
startIcon={<ShoppingCartIcon />}
|
|
sx={{
|
|
borderRadius: 2,
|
|
fontWeight: "bold",
|
|
backgroundColor: "#9ccc65", // yellowish green
|
|
color: "#000000",
|
|
"&:hover": {
|
|
backgroundColor: "#8bc34a",
|
|
},
|
|
}}
|
|
>
|
|
{/*this.props.steckling ?
|
|
(this.props.t ? this.props.t('cart.preorderCutting') : "Als Steckling vorbestellen") : */
|
|
(this.props.t ? this.props.t('cart.addToCart') : "In den Korb")}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// If items are in cart, show quantity controls with yellowish green
|
|
if (quantity > 0) {
|
|
return (
|
|
<Box sx={{ width: "100%" }}>
|
|
<ButtonGroup
|
|
fullWidth
|
|
variant="contained"
|
|
color="primary"
|
|
size={size || "medium"}
|
|
sx={{
|
|
borderRadius: 2,
|
|
"& .MuiButtonGroup-grouped:not(:last-of-type)": {
|
|
borderRight: "1px solid rgba(255,255,255,0.3)",
|
|
},
|
|
}}
|
|
>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleDecrement}
|
|
aria-label="Menge verringern"
|
|
sx={{ width: "28px", borderRadius: 0, flexGrow: 1 }}
|
|
>
|
|
<RemoveIcon />
|
|
</IconButton>
|
|
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
px: 2,
|
|
flexGrow: 2,
|
|
position: "relative",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={this.handleEditStart}
|
|
>
|
|
{isEditing ? (
|
|
<TextField
|
|
autoFocus
|
|
value={editValue}
|
|
onChange={this.handleEditChange}
|
|
onBlur={this.handleEditComplete}
|
|
onKeyPress={this.handleKeyPress}
|
|
onFocus={(e) => e.target.select()}
|
|
size="small"
|
|
variant="standard"
|
|
inputProps={{
|
|
style: {
|
|
textAlign: "center",
|
|
width: "30px",
|
|
fontSize: "14px",
|
|
padding: "2px",
|
|
fontWeight: "bold",
|
|
},
|
|
"aria-label": "quantity",
|
|
}}
|
|
sx={{ my: -0.5 }}
|
|
/>
|
|
) : (
|
|
<Typography variant="button" sx={{ fontWeight: "bold" }}>
|
|
{quantity}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleIncrement}
|
|
aria-label="Menge erhöhen"
|
|
sx={{ width: "28px", borderRadius: 0, flexGrow: 1 }}
|
|
>
|
|
<AddIcon />
|
|
</IconButton>
|
|
|
|
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleClearCart}
|
|
aria-label={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'}
|
|
sx={{
|
|
borderRadius: 0,
|
|
"&:hover": { color: "error.light" },
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
{this.props.cartButton && (
|
|
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.toggleCart}
|
|
aria-label={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'}
|
|
sx={{
|
|
borderRadius: 0,
|
|
"&:hover": { color: "primary.light" },
|
|
}}
|
|
>
|
|
<ShoppingCartIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</ButtonGroup>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
return (
|
|
<Button
|
|
disabled
|
|
fullWidth
|
|
variant="contained"
|
|
size={size || "medium"}
|
|
sx={{
|
|
borderRadius: 2,
|
|
fontWeight: "bold",
|
|
}}
|
|
>
|
|
{this.props.t ? this.props.t('product.outOfStock') : 'Out of Stock'}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
|
|
|
|
// If no items in cart, show simple "Add to Cart" button
|
|
if (quantity === 0) {
|
|
return (
|
|
<Button
|
|
fullWidth
|
|
variant="contained"
|
|
color="primary"
|
|
size={size || "medium"}
|
|
onClick={this.handleIncrement}
|
|
startIcon={<ShoppingCartIcon />}
|
|
sx={{
|
|
borderRadius: 2,
|
|
fontWeight: "bold",
|
|
whiteSpace: "nowrap",
|
|
"&:hover": {
|
|
backgroundColor: "primary.dark",
|
|
},
|
|
}}
|
|
>
|
|
{/*this.props.steckling ?
|
|
(this.props.t ? this.props.t('cart.preorderCutting') : "Als Steckling vorbestellen") :*/
|
|
(this.props.t ? this.props.t('cart.addToCart') : "In den Korb")}
|
|
</Button>
|
|
);
|
|
}
|
|
|
|
// If items are in cart, show quantity controls
|
|
return (
|
|
<Box sx={{ width: "100%" }}>
|
|
<ButtonGroup
|
|
fullWidth
|
|
variant="contained"
|
|
color="primary"
|
|
size={size || "medium"}
|
|
sx={{
|
|
borderRadius: 2,
|
|
"& .MuiButtonGroup-grouped:not(:last-of-type)": {
|
|
borderRight: "1px solid rgba(255,255,255,0.3)",
|
|
},
|
|
}}
|
|
>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleDecrement}
|
|
aria-label="Menge verringern"
|
|
sx={{ width: "28px", borderRadius: 0, flexGrow: 1 }}
|
|
>
|
|
<RemoveIcon />
|
|
</IconButton>
|
|
|
|
<Box
|
|
sx={{
|
|
display: "flex",
|
|
alignItems: "center",
|
|
justifyContent: "center",
|
|
px: 2,
|
|
flexGrow: 2,
|
|
position: "relative",
|
|
cursor: "pointer",
|
|
}}
|
|
onClick={this.handleEditStart}
|
|
>
|
|
{isEditing ? (
|
|
<TextField
|
|
autoFocus
|
|
value={editValue}
|
|
onChange={this.handleEditChange}
|
|
onBlur={this.handleEditComplete}
|
|
onKeyPress={this.handleKeyPress}
|
|
onFocus={(e) => e.target.select()}
|
|
size="small"
|
|
variant="standard"
|
|
inputProps={{
|
|
style: {
|
|
textAlign: "center",
|
|
width: "30px",
|
|
fontSize: "14px",
|
|
padding: "2px",
|
|
fontWeight: "bold",
|
|
},
|
|
"aria-label": "quantity",
|
|
}}
|
|
sx={{ my: -0.5 }}
|
|
/>
|
|
) : (
|
|
<Typography variant="button" sx={{ fontWeight: "bold" }}>
|
|
{quantity}
|
|
</Typography>
|
|
)}
|
|
</Box>
|
|
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleIncrement}
|
|
aria-label="Menge erhöhen"
|
|
sx={{ width: "28px", borderRadius: 0, flexGrow: 1 }}
|
|
>
|
|
<AddIcon />
|
|
</IconButton>
|
|
|
|
<Tooltip title={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'} arrow>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.handleClearCart}
|
|
aria-label={this.props.t ? this.props.t('cart.removeFromCart') : 'Aus dem Warenkorb entfernen'}
|
|
sx={{
|
|
borderRadius: 0,
|
|
"&:hover": { color: "error.light" },
|
|
}}
|
|
>
|
|
<DeleteIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
{this.props.cartButton && (
|
|
<Tooltip title={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'} arrow>
|
|
<IconButton
|
|
color="inherit"
|
|
onClick={this.toggleCart}
|
|
aria-label={this.props.t ? this.props.t('cart.openCart') : 'Warenkorb öffnen'}
|
|
sx={{
|
|
borderRadius: 0,
|
|
"&:hover": { color: "primary.light" },
|
|
}}
|
|
>
|
|
<ShoppingCartIcon />
|
|
</IconButton>
|
|
</Tooltip>
|
|
)}
|
|
</ButtonGroup>
|
|
</Box>
|
|
);
|
|
}
|
|
}
|
|
|
|
export default withI18n()(AddToCartButton);
|