diff options
Diffstat (limited to 'views')
| -rw-r--r-- | views/css/smartcheckout.css | 317 | ||||
| -rw-r--r-- | views/js/smartcheckout.js | 694 | ||||
| -rw-r--r-- | views/mods/mod_basketteaser/smartcheckout.phtml | 44 | ||||
| -rw-r--r-- | views/mods/mod_orderupload/smartcheckout_checkout.phtml | 77 | ||||
| -rw-r--r-- | views/mods/mod_orderupload/smartcheckout_row.phtml | 21 | ||||
| -rw-r--r-- | views/mods/mod_paypalapi/overview_preauth_button.phtml | 15 | ||||
| -rw-r--r-- | views/mods/mod_productvars/smartcheckout_row.phtml | 57 |
7 files changed, 1225 insertions, 0 deletions
diff --git a/views/css/smartcheckout.css b/views/css/smartcheckout.css new file mode 100644 index 0000000..6d9ac8b --- /dev/null +++ b/views/css/smartcheckout.css @@ -0,0 +1,317 @@ +@CHARSET "UTF-8"; + +/* + * Moderner EinSeitenCheckout + * + * Die Optik hält sich bewusst zurück: Schrift, Textfarbe und Hintergrund kommen aus dem + * Theme, eigene Angaben gibt es nur dort, wo die Struktur sie braucht. Linien und Flächen + * sind neutrales Grau mit Transparenz und wirken deshalb auf hellen wie auf dunklen + * Themes. Gedämpfter Text entsteht über opacity statt über eine eigene Farbe. + * + * Die wenigen Werte liegen als Custom Properties auf .wpsg_smartcheckout und können im + * Theme überschrieben werden, ohne dass eine Regel angefasst werden muss. + */ + +.wpsg_smartcheckout { + --sc-line:rgba(127, 127, 127, 0.35); + --sc-line-soft:rgba(127, 127, 127, 0.2); + --sc-flat:rgba(127, 127, 127, 0.08); + --sc-accent:currentColor; + --sc-error:#b3261e; + --sc-radius:3px; + --sc-gap:2rem; +} + +.wpsg_smartcheckout [v-cloak], .wpsg_smartcheckout[v-cloak] { display:none; } +.wpsg_smartcheckout_noscript { padding:1rem; border-left:3px solid var(--sc-error, #b3261e); margin-bottom:1.5rem; } + +.wpsg_smartcheckout * { box-sizing:border-box; } + +/* Themes mit Inhaltskarte (z.B. Kadence "content-style-boxed") ziehen alignwide bis an den Kartenrand, der Checkout bleibt im Innenabstand */ +html body.content-style-boxed .entry-content .wpsg_smartcheckout.alignwide, +html body.content-style-boxed .entry-content .wpsg_smartcheckout_after.alignwide { margin-left:0; margin-right:0; width:auto; max-width:100%; } + +/* Layout */ +.wpsg_smartcheckout .sc_layout { display:grid; grid-template-columns:minmax(0, 1fr) 21rem; gap:var(--sc-gap); align-items:start; } +.wpsg_smartcheckout_single .sc_layout { grid-template-columns:minmax(0, 1fr); } +.wpsg_smartcheckout .sc_main { display:flex; flex-direction:column; min-width:0; } +.wpsg_smartcheckout .sc_side { position:sticky; top:1.5rem; } +.wpsg_smartcheckout_single .sc_side { position:static; } +.wpsg_smartcheckout .sc_layout.sc_busy { opacity:0.6; pointer-events:none; transition:opacity 0.15s ease-out; } + +/* Die Schritte werden nur durch eine Linie getrennt, nicht durch gerahmte Kästen */ +.wpsg_smartcheckout .sc_box { padding:var(--sc-gap) 0; border-top:1px solid var(--sc-line-soft); } +.wpsg_smartcheckout .sc_box:first-child { padding-top:0; border-top:0; } +.wpsg_smartcheckout .sc_title { margin:0 0 1.25rem 0; font-size:1.05rem; line-height:1.3; } +.wpsg_smartcheckout .sc_step { margin-right:0.5rem; font-weight:400; opacity:0.5; } + +/* Meldungen */ +.wpsg_smartcheckout .sc_messages { display:flex; flex-direction:column; gap:0.5rem; margin-bottom:var(--sc-gap); } +.wpsg_smartcheckout .sc_messages:empty { display:none; margin:0; } +.wpsg_smartcheckout .sc_message { padding:0.6rem 0.9rem; border-left:3px solid var(--sc-line); background:var(--sc-flat); } +.wpsg_smartcheckout .sc_message_error { border-left-color:var(--sc-error); } +.wpsg_smartcheckout .sc_message_message { border-left-color:#5a9e3d; } + +/* Leerer Warenkorb */ +.wpsg_smartcheckout .sc_empty { padding:2.5rem 0; } + +/* Warenkorbpositionen: jede Position steht in einem eigenen Rahmen, damit Produktvariablen und Hinweise erkennbar zum Produkt gehören */ +.wpsg_smartcheckout .sc_products { display:flex; flex-direction:column; gap:0.75rem; } +.wpsg_smartcheckout .sc_product { display:grid; grid-template-columns:auto minmax(0, 1fr) auto auto; gap:1rem; align-items:start; padding:1rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); } +.wpsg_smartcheckout .sc_product_image img { display:block; width:56px; height:56px; object-fit:cover; border-radius:var(--sc-radius); } +.wpsg_smartcheckout .sc_product_name { font-weight:600; } +.wpsg_smartcheckout .sc_product_desc, +.wpsg_smartcheckout .sc_product_extra, +.wpsg_smartcheckout .sc_product_tax { font-size:0.85rem; opacity:0.7; margin-top:0.2rem; } + +/* Eingabefelder der Module (z.B. Produktvariablen) als eigene Zeile über alle Spalten der Position, innerhalb des Rahmens durch eine Linie abgesetzt */ +.wpsg_smartcheckout .sc_product_fields { grid-column:1 / -1; display:flex; flex-direction:column; gap:1rem; min-width:0; padding-top:0.75rem; border-top:1px solid var(--sc-line-soft); font-size:0.85rem; } + +/* Upload zum Produkt (wpsg_mod_orderupload) an der Warenkorbposition + * + * Das Ziel wird nach dem Hochladen vom Modul per Script neu befüllt, die Regeln hängen deshalb am + * klassischen Markup (ul, input[type=button]). Die Spezifität liegt über den Button-Regeln der Themes. + */ +.wpsg_smartcheckout .sc_upload { display:flex; flex-direction:column; gap:0.4rem; } +.wpsg_smartcheckout .sc_upload_label { font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_upload_target { display:flex; flex-wrap:wrap; align-items:center; gap:0.5rem; } +.wpsg_smartcheckout .sc_upload_target ul { flex:1 1 100%; display:flex; flex-direction:column; gap:0.3rem; margin:0; padding:0; list-style:none; } +.wpsg_smartcheckout .sc_upload_target li { display:flex; align-items:center; gap:0.5rem; margin:0; padding:0.35rem 0.6rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); } +.wpsg_smartcheckout .sc_upload_target li > a:first-child { flex:1 1 auto; min-width:0; overflow-wrap:anywhere; color:inherit; } +.wpsg_smartcheckout .sc_upload_target .size { font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_upload_target .removeLink { flex:0 0 auto; display:flex; opacity:0.7; } +.wpsg_smartcheckout .sc_upload_target .removeLink:hover { opacity:1; } +.wpsg_smartcheckout .sc_upload_target .removeLink img { display:block; width:16px; height:16px; } +.wpsg_smartcheckout .sc_upload_target input[type="file"] { flex:1 1 14rem; min-width:0; } +.wpsg_smartcheckout .sc_upload_target input[type="button"] { flex:0 0 auto; min-height:0; margin:0; padding:0.5rem 0.9rem; border:1px solid var(--sc-line); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; font-size:0.9rem; line-height:1.2; text-transform:none; letter-spacing:normal; box-shadow:none; cursor:pointer; } +.wpsg_smartcheckout .sc_upload_target input[type="button"]:hover { background:var(--sc-flat); } +/* Der Upload startet direkt nach der Auswahl (smartcheckout.js, startUpload), der Button des Moduls wird nicht gebraucht */ +.wpsg_smartcheckout .sc_upload_target [data-sc-autoupload] { display:none; } + +/* Bestellupload (wpsg_mod_orderupload) als eigener Block unter den Kundendaten, Liste und Dateiauswahl wie beim Upload zum Produkt */ +.wpsg_smartcheckout .sc_orderupload .sc_title { margin-bottom:1rem; } +.wpsg_smartcheckout .sc_orderupload .sc_hint { margin-top:0; } +.wpsg_smartcheckout .sc_orderupload .sc_upload { font-size:0.85rem; } +.wpsg_smartcheckout .sc_upload_message { flex:1 1 100%; padding:0.35rem 0.6rem; border-left:3px solid var(--sc-line); background:var(--sc-flat); } +.wpsg_smartcheckout .sc_upload_message_error { border-left-color:var(--sc-error); } +.wpsg_smartcheckout .sc_upload_message_message { border-left-color:#5a9e3d; } +.wpsg_smartcheckout .sc_upload_wait { flex:1 1 100%; opacity:0.7; } + +/* Dateiauswahl im gesamten Checkout (Upload zum Produkt, Bestellupload) in der Optik der übrigen Felder */ +.wpsg_smartcheckout input[type="file"] { max-width:100%; color:inherit; font:inherit; font-size:0.85rem; } +.wpsg_smartcheckout input[type="file"]::file-selector-button { margin-right:0.6rem; padding:0.45rem 0.8rem; border:1px solid var(--sc-line); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; cursor:pointer; } +.wpsg_smartcheckout input[type="file"]::file-selector-button:hover { background:var(--sc-flat); } + +/* Produktvariablen an der Warenkorbposition */ +.wpsg_smartcheckout .sc_pvars { display:flex; flex-direction:column; gap:0.6rem; } +.wpsg_smartcheckout .sc_pvar { display:flex; flex-direction:column; gap:0.25rem; } +.wpsg_smartcheckout .sc_pvar_label { font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_pvar_input { width:100%; padding:0.4rem 0.6rem; border:1px solid var(--sc-line); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; } +.wpsg_smartcheckout textarea.sc_pvar_input { resize:vertical; } +.wpsg_smartcheckout .sc_pvar_check { display:flex; align-items:flex-start; gap:0.5rem; cursor:pointer; } +.wpsg_smartcheckout .sc_pvar_check input[type="checkbox"] { flex:0 0 auto; margin:0.15rem 0 0 0; } +.wpsg_smartcheckout .sc_pvar.sc_invalid .sc_pvar_input { border-color:var(--sc-error); } +.wpsg_smartcheckout .sc_pvar.sc_invalid .sc_pvar_label, +.wpsg_smartcheckout .sc_pvar.sc_invalid .sc_pvar_check { color:var(--sc-error); opacity:1; } +.wpsg_smartcheckout .sc_product_price { text-align:right; white-space:nowrap; } +.wpsg_smartcheckout .sc_product_price_total { font-weight:600; } +.wpsg_smartcheckout .sc_product_price_single { font-size:0.8rem; opacity:0.7; } + +/* Mengenwahl */ +.wpsg_smartcheckout .sc_product_amount { display:flex; flex-direction:column; align-items:center; gap:0.35rem; } +.wpsg_smartcheckout .sc_stepper { display:inline-flex; align-items:stretch; border:1px solid var(--sc-line); border-radius:var(--sc-radius); overflow:hidden; } +.wpsg_smartcheckout .sc_stepper_btn { border:0; background:none; width:1.9rem; cursor:pointer; font:inherit; line-height:1; color:inherit; } +.wpsg_smartcheckout .sc_stepper_btn:disabled { cursor:default; opacity:0.4; } +.wpsg_smartcheckout .sc_stepper_input { width:2.8rem; border:0; text-align:center; padding:0.4rem 0; background:none; color:inherit; font:inherit; } +.wpsg_smartcheckout .sc_stepper_static { display:inline-block; padding:0.4rem 0.9rem; } +.wpsg_smartcheckout .sc_remove { border:0; background:none; padding:0; font-size:0.8rem; opacity:0.7; text-decoration:underline; cursor:pointer; color:inherit; } + +/* Warenkorbteaser: angelehnt an die Warenkorbpositionen, aber zurückhaltender */ +.wpsg_smartcheckout .sc_teaser { padding-top:1rem; border-top:1px solid var(--sc-line-soft); } +.wpsg_smartcheckout .sc_teaser_title { margin-bottom:0.5rem; font-size:0.85rem; opacity:0.7; } +.wpsg_smartcheckout .sc_teaser_item { display:flex; align-items:center; gap:0.75rem 1rem; flex-wrap:wrap; padding:0.5rem 0; font-size:0.9rem; } +.wpsg_smartcheckout .sc_teaser_image img { display:block; width:40px; height:40px; object-fit:cover; border-radius:var(--sc-radius); } +.wpsg_smartcheckout .sc_teaser_body { flex:1 1 10rem; min-width:0; } +.wpsg_smartcheckout .sc_teaser_price { font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_teaser_action { display:flex; align-items:center; gap:0.5rem; } +.wpsg_smartcheckout .sc_teaser_amount { width:2.8rem; padding:0.45rem 0; border:1px solid var(--sc-line); border-radius:var(--sc-radius); text-align:center; background:none; color:inherit; font:inherit; } + +/* Gutschein */ +.wpsg_smartcheckout .sc_voucher { margin-top:1.5rem; } +.wpsg_smartcheckout .sc_voucher_label { display:block; margin-bottom:0.35rem; font-size:0.85rem; opacity:0.7; } +.wpsg_smartcheckout .sc_voucher_row { display:flex; gap:0.5rem; flex-wrap:wrap; } +.wpsg_smartcheckout .sc_voucher_input { flex:1 1 12rem; min-width:0; } +.wpsg_smartcheckout .sc_voucher_list { margin-top:0.75rem; display:flex; flex-direction:column; gap:0.4rem; } +.wpsg_smartcheckout .sc_voucher_item { display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap; font-size:0.9rem; } +.wpsg_smartcheckout .sc_voucher_code { font-weight:600; } +.wpsg_smartcheckout .sc_voucher_partial { opacity:0.7; } +.wpsg_smartcheckout .sc_voucher_value { margin-left:auto; } + +/* Formularfelder */ +.wpsg_smartcheckout .sc_fields { display:grid; grid-template-columns:repeat(12, 1fr); gap:0.9rem; } +.wpsg_smartcheckout .sc_field { display:flex; flex-direction:column; gap:0.3rem; grid-column:span 12; min-width:0; } +.wpsg_smartcheckout .sc_field_half { grid-column:span 6; } +.wpsg_smartcheckout .sc_field_wide { grid-column:span 8; } +.wpsg_smartcheckout .sc_field_narrow { grid-column:span 4; } +.wpsg_smartcheckout .sc_label { font-size:0.85rem; opacity:0.7; } +.wpsg_smartcheckout .sc_required { color:var(--sc-error); margin-left:0.15rem; } +.wpsg_smartcheckout .sc_input, +.wpsg_smartcheckout .sc_voucher_input, +.wpsg_smartcheckout .sc_textarea { width:100%; padding:0.55rem 0.7rem; border:1px solid var(--sc-line); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; } +.wpsg_smartcheckout .sc_textarea { resize:vertical; } +/* Auswahlfelder mit eigenem Pfeil: der Hintergrund der Felder wird zurückgesetzt, damit verschwindet ein vom Theme gezeichneter Pfeil */ +#wpsg_smartcheckout select.sc_input, +#wpsg_smartcheckout select.sc_pvar_input, +#wpsg_smartcheckout .sc_modul_extra select { -webkit-appearance:none; appearance:none; padding-right:2rem; background-color:transparent; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 8'%3E%3Cpath d='M1 1l5 5 5-5' fill='none' stroke='%237f7f7f' stroke-width='1.6'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 0.7rem center; background-size:0.7rem auto; cursor:pointer; } +.wpsg_smartcheckout .sc_hint { margin-top:1rem; font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_modul_login:empty, +.wpsg_smartcheckout .sc_modul_customer:empty, +.wpsg_smartcheckout .sc_modul_payment:empty { display:none; } +.wpsg_smartcheckout .sc_modul_basket, +.wpsg_smartcheckout .sc_modul_customer, +.wpsg_smartcheckout .sc_modul_payment, +.wpsg_smartcheckout .sc_modul_sum { margin-top:1rem; } +.wpsg_smartcheckout .sc_modul_sum:empty { display:none; } + +/* Modulblöcke vor dem Abschluss (Bestellvariablen, gesonderte Lieferadresse, …) + * + * Diese Module bringen das Markup des klassischen Checkouts mit (wpsg_box, wpsg_inner, + * wpsg_checkoutblock). Die Regeln dazu in frontend.css hängen an .wpsg_checkout und + * greifen hier nicht — die Felder werden deshalb an dieser Stelle auf die Optik des + * EinSeitenCheckouts gebracht. + */ +.wpsg_smartcheckout .sc_modul_extra > div + div { margin-top:var(--sc-gap); } +.wpsg_smartcheckout .sc_modul_extra h2 { margin:0 0 1.25rem 0; padding:0; font-size:1.05rem; line-height:1.3; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_spacer, +.wpsg_smartcheckout .sc_modul_extra .wpsg_clear { display:none; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_inner { display:flex; flex-direction:column; gap:0.9rem; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_checkoutblock { display:flex; flex-direction:column; gap:0.3rem; } +.wpsg_smartcheckout .sc_modul_extra label { font-size:0.85rem; opacity:0.7; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_required { color:var(--sc-error); margin-left:0.15rem; } +.wpsg_smartcheckout .sc_modul_extra input[type="text"], +.wpsg_smartcheckout .sc_modul_extra input[type="tel"], +.wpsg_smartcheckout .sc_modul_extra input[type="email"], +.wpsg_smartcheckout .sc_modul_extra input[type="number"], +.wpsg_smartcheckout .sc_modul_extra select, +.wpsg_smartcheckout .sc_modul_extra textarea { width:100%; padding:0.55rem 0.7rem; border:1px solid var(--sc-line); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_error { border-color:var(--sc-error); } + +/* Ankreuzfelder: Kästchen und Text stehen in einer Zeile, der Text bleibt bei einem Umbruch bündig */ +.wpsg_smartcheckout .sc_modul_extra .wpsg_mod_ordervars_check label { display:flex; align-items:flex-start; gap:0.5rem; opacity:1; cursor:pointer; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_mod_ordervars_check label > input[type="checkbox"] { flex:0 0 auto; width:auto; margin:0; } +.wpsg_smartcheckout .sc_modul_extra .wpsg_mod_ordervars_text { flex:1 1 auto; min-width:0; } + +/* Versand- und Zahlungsauswahl */ +.wpsg_smartcheckout .sc_choices { display:flex; flex-direction:column; gap:0.5rem; } +.wpsg_smartcheckout .sc_choice { --sc-choice-radio:1rem; --sc-choice-gap:0.6rem; display:flex; flex-direction:column; gap:0.3rem; padding:0.8rem 0.9rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); cursor:pointer; } +.wpsg_smartcheckout .sc_choice_active { border-color:var(--sc-accent); background:var(--sc-flat); } +/* Kopfzeile: Radio, Logo, Bezeichnung und Preis auf einer Linie */ +.wpsg_smartcheckout .sc_choice_head { display:flex; align-items:center; gap:var(--sc-choice-gap); min-width:0; } +.wpsg_smartcheckout .sc_choice_head input[type="radio"] { flex:0 0 auto; width:var(--sc-choice-radio); height:var(--sc-choice-radio); margin:0; } +/* Feste Breite, damit die Bezeichnungen bei unterschiedlich breiten Logos bündig untereinander stehen */ +.wpsg_smartcheckout .sc_choice_logo { flex:0 0 auto; width:4.5rem; height:22px; object-fit:contain; object-position:left center; } +.wpsg_smartcheckout .sc_choice_name { flex:1 1 auto; min-width:0; font-weight:600; } +.wpsg_smartcheckout .sc_choice_price { flex:0 0 auto; margin-left:auto; white-space:nowrap; } +/* Beschreibung und Zusatzangaben darunter, bündig mit Logo bzw. Bezeichnung */ +.wpsg_smartcheckout .sc_choice_hint, +.wpsg_smartcheckout .sc_choice_extra { padding-left:calc(var(--sc-choice-radio) + var(--sc-choice-gap)); font-size:0.85rem; } +.wpsg_smartcheckout .sc_choice_hint { opacity:0.7; } + +/* Zusammenfassung */ +.wpsg_smartcheckout .sc_summary { position:relative; padding:1.25rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); } +.wpsg_smartcheckout_single .sc_side { margin-top:var(--sc-gap); } +.wpsg_smartcheckout .sc_sum_rows { display:flex; flex-direction:column; gap:0.4rem; } +.wpsg_smartcheckout .sc_sum_row { display:flex; justify-content:space-between; gap:1rem; align-items:baseline; font-size:0.95rem; } +.wpsg_smartcheckout .sc_sum_value { white-space:nowrap; } +.wpsg_smartcheckout .sc_sum_note, +.wpsg_smartcheckout .sc_sum_hint { font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_sum_hint { margin-top:0.75rem; } +.wpsg_smartcheckout .sc_sum_total { margin-top:0.5rem; padding-top:0.6rem; border-top:1px solid var(--sc-line); font-size:1.05rem; font-weight:600; } + +/* Zusatzzeilen der Module (z.B. Gesamtgewicht) bringen das Markup der klassischen Zusammenfassung mit, sie erscheinen gedämpft wie die Hinweise */ +.wpsg_smartcheckout .sc_modul_sum .overview_sum_row { display:flex; justify-content:space-between; gap:1rem; align-items:baseline; font-size:0.8rem; opacity:0.7; } +.wpsg_smartcheckout .sc_modul_sum .overview_sum_row .title, +.wpsg_smartcheckout .sc_modul_sum .overview_sum_row .value { width:auto; margin:0; font-weight:normal; } +.wpsg_smartcheckout .sc_modul_sum .overview_sum_row .value { white-space:nowrap; } +.wpsg_smartcheckout .sc_modul_sum .overview_sum_row .value + .value { display:none; } + +/* Bestellbedingungen und AGB */ +.wpsg_smartcheckout .sc_modul_conditions, +.wpsg_smartcheckout .sc_agb { margin-top:1.1rem; font-size:0.85rem; } +.wpsg_smartcheckout .sc_modul_conditions .wpsg_agb + .wpsg_agb { margin-top:0.6rem; } +/* Die Module setzen den Text als losen Textknoten ins label — Flex würde ihn zerreißen */ +.wpsg_smartcheckout .sc_modul_conditions label, +.wpsg_smartcheckout .sc_agb label { display:block; position:relative; padding-left:1.5rem; cursor:pointer; } +.wpsg_smartcheckout .sc_modul_conditions label > input[type="checkbox"], +.wpsg_smartcheckout .sc_agb label > input[type="checkbox"] { position:absolute; left:0; top:0.15rem; margin:0; } +.wpsg_smartcheckout .sc_modul_conditions .wpsg_mod_ordercondition_hint { max-height:9rem; overflow-y:auto; margin-top:0.5rem; padding:0.6rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); } + +/* Fehlerhafte Eingaben */ +.wpsg_smartcheckout .sc_field.sc_invalid .sc_input, +.wpsg_smartcheckout .sc_invalid.sc_voucher_input { border-color:var(--sc-error); } +.wpsg_smartcheckout .sc_choices.sc_invalid, +.wpsg_smartcheckout .sc_modul_conditions.sc_invalid, +.wpsg_smartcheckout .sc_agb.sc_invalid { outline:1px solid var(--sc-error); outline-offset:4px; } + +/* Buttons erben Schrift und Farbe des Themes und bleiben bewusst schlicht */ +.wpsg_smartcheckout .sc_button { display:inline-flex; align-items:center; justify-content:center; padding:0.65rem 1.1rem; border:1px solid var(--sc-accent); border-radius:var(--sc-radius); background:none; color:inherit; font:inherit; cursor:pointer; } +.wpsg_smartcheckout .sc_button:hover { background:var(--sc-flat); } +.wpsg_smartcheckout .sc_button:disabled { opacity:0.5; cursor:default; } +.wpsg_smartcheckout .sc_button_secondary { padding:0.5rem 0.9rem; border-color:var(--sc-line); font-size:0.9rem; } +.wpsg_smartcheckout .sc_button_order { width:100%; margin-top:1.1rem; font-weight:600; } + +/* Ladeanzeige */ +.wpsg_smartcheckout .sc_spinner { position:absolute; top:1.25rem; right:1.25rem; } +.wpsg_smartcheckout .sc_spinner_dot { display:block; width:0.9rem; height:0.9rem; border:2px solid var(--sc-line); border-top-color:var(--sc-accent); border-radius:50%; animation:wpsg_sc_spin 0.7s linear infinite; } + +/* Layer beim Abschicken der Bestellung über der gesamten Seite */ +.wpsg_smartcheckout .sc_submitting { position:fixed; top:0; left:0; right:0; bottom:0; z-index:100001; display:flex; align-items:center; justify-content:center; padding:1rem; background-color:rgba(255, 255, 255, 0.75); cursor:wait; } +.wpsg_smartcheckout .sc_submitting_inner { display:flex; align-items:center; gap:0.75rem; padding:1rem 1.25rem; border:1px solid var(--sc-line-soft); border-radius:var(--sc-radius); background-color:#FFFFFF; color:#1d1d1d; box-shadow:0 4px 20px rgba(0, 0, 0, 0.08); } +.wpsg_smartcheckout .sc_submitting .sc_spinner_dot { width:1.2rem; height:1.2rem; } +.wpsg_smartcheckout .sc_label_hidden, +.wpsg_smartcheckout .sc_visuallyhidden { position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; } + +/* GARAN-Gewährleistungslabel */ + +.wpsg_smartcheckout .sc_garan { text-align:center; margin:1rem 0; } +.wpsg_smartcheckout .sc_garan img { display:block; max-width:100%; height:auto; } +.wpsg_smartcheckout .sc_garan_zoom { display:inline-block; padding:0; border:0; background:none; cursor:zoom-in; } +.wpsg_smartcheckout .sc_garan_zoom:focus-visible { outline:2px solid currentColor; outline-offset:2px; } +.wpsg_smartcheckout .sc_garan_lightbox { cursor:zoom-out; } +.wpsg_smartcheckout .sc_garan_lightbox .sc_garan_popup_inner { cursor:default; } +.wpsg_smartcheckout .sc_garan_popup { position:fixed; top:0; left:0; right:0; bottom:0; z-index:100000; display:flex; align-items:center; justify-content:center; padding:1rem; background-color:rgba(0, 0, 0, 0.6); } +.wpsg_smartcheckout .sc_garan_popup_inner { display:flex; flex-direction:column; max-width:600px; max-height:100%; padding:1rem; text-align:center; background-color:#FFFFFF; } +.wpsg_smartcheckout .sc_garan_popup_image { min-height:0; overflow-y:auto; } +.wpsg_smartcheckout .sc_garan_popup_image img { max-width:100%; height:auto; } +.wpsg_smartcheckout .sc_garan_popup_inner .sc_button { flex:0 0 auto; margin-top:1rem; } +.wpsg_smartcheckout .sc_product_revocation { margin-top:0.5rem; } + +@keyframes wpsg_sc_spin { + to { transform:rotate(360deg); } +} + +@media (prefers-reduced-motion: reduce) { + .wpsg_smartcheckout .sc_spinner_dot { animation:none; } + .wpsg_smartcheckout .sc_layout.sc_busy { transition:none; } +} + +/* Schmale Ansichten */ +@media (max-width: 900px) { + .wpsg_smartcheckout .sc_layout { grid-template-columns:minmax(0, 1fr); } + .wpsg_smartcheckout .sc_side { position:static; margin-top:var(--sc-gap); } +} + +@media (max-width: 600px) { + .wpsg_smartcheckout .sc_summary { padding:1rem; } + .wpsg_smartcheckout .sc_product { padding:0.75rem; } + /* Ohne Produktbild bliebe im Raster eine leere Spalte stehen, deshalb hier Flex */ + .wpsg_smartcheckout .sc_product { display:flex; flex-wrap:wrap; align-items:center; gap:0.75rem 1rem; } + .wpsg_smartcheckout .sc_product_image { flex:0 0 auto; } + .wpsg_smartcheckout .sc_product_body { flex:1 1 60%; min-width:0; } + .wpsg_smartcheckout .sc_product_fields { flex:1 1 100%; } + .wpsg_smartcheckout .sc_product_amount { flex:0 0 auto; align-items:flex-start; } + .wpsg_smartcheckout .sc_product_price { flex:1 1 auto; } + .wpsg_smartcheckout .sc_field_half, + .wpsg_smartcheckout .sc_field_wide, + .wpsg_smartcheckout .sc_field_narrow { grid-column:span 12; } +} diff --git a/views/js/smartcheckout.js b/views/js/smartcheckout.js new file mode 100644 index 0000000..df7721a --- /dev/null +++ b/views/js/smartcheckout.js @@ -0,0 +1,694 @@ +/** + * Vue-Anwendung des modernen EinSeitenCheckouts + * + * Die Anwendung rechnet selbst nichts. Jede Änderung wird als Formulardaten an die + * Checkout-Seite geschickt, dort serverseitig neu berechnet und als vollständiger + * Zustand zurückgeliefert. + */ + +(function() { + + 'use strict'; + + var el_root = document.getElementById('wpsg_smartcheckout'); + var el_state = document.getElementById('wpsg_smartcheckout_state'); + + if (el_root === null || el_state === null || typeof Vue === 'undefined') return; + + var initialState = null; + + try { + + initialState = JSON.parse(el_state.textContent); + + } catch (e) { + + return; + + } + + Vue.createApp({ + + data: function() { + + return { + state: initialState, + form: {}, + amounts: {}, + voucher: '', + voucherError: false, + agb: false, + garanPopup: false, + garanConfirmed: false, + garanSubmit: false, + garanLightbox: false, + busy: false, + queued: false, + queuedUpload: null, + submitting: false, + timer: null + }; + + }, + + created: function() { + + this.syncForm(true); + + // Bereits akzeptierte AGB, z.B. nach der Rückkehr von PayPal + this.agb = (this.state.accepted && this.state.accepted.agb === true); + + }, + + mounted: function() { + + // Nach einem abgelehnten Bestellversuch steht der Kunde vor dem ersten Fehler + if (this.state.errors.length !== 0) this.$nextTick(this.focusError); + + // Das GARAN-Gewährleistungslabel erscheint wie in der klassischen Zusammenfassung beim Betreten der Seite + if (this.state.warranty && this.state.warranty.popup === true && this.state.empty !== true) this.garanPopup = true; + + // Die Lightbox des Gewährleistungslabels lässt sich mit Esc schließen + var self = this; + + document.addEventListener('keydown', function(event) { + + if (event.key === 'Escape' && self.garanLightbox === true) self.garanLightbox = false; + + }); + + // Zurück aus dem Seiten-Cache des Browsers (z.B. Zurück-Button nach dem Zahlungsanbieter): Layer wieder entfernen + window.addEventListener('pageshow', function(event) { + + if (event.persisted) self.submitting = false; + + }); + + // Eingaben in den Blöcken der Module haben kein Vue-Binding, Änderungen werden hier abgefangen + el_root.addEventListener('change', this.onModuleChange); + + // Buttons in den Blöcken der Module, die eine Menge an den Server schicken (z.B. Warenkorbteaser) + el_root.addEventListener('click', this.onModuleClick); + el_root.addEventListener('keydown', this.onModuleKeydown); + + }, + + methods: { + + /** + * Übernimmt den Zustand des Servers in die Formularfelder + * + * Textfelder werden nur beim ersten Aufbau gesetzt — sonst würde eine Antwort, + * die während des Tippens eintrifft, die Eingabe des Kunden überschreiben. + */ + syncForm: function(full) { + + var self = this; + + if (full === true) { + + Object.keys(this.state.checkout).forEach(function(key) { + + self.form[key] = self.state.checkout[key]; + + }); + + } + + this.form.shipping = this.state.shipping.selected; + this.form.payment = this.state.payment.selected; + + this.amounts = {}; + + this.state.products.forEach(function(product) { + + self.amounts[product.index] = product.amount; + + }); + + }, // syncForm(full) + + /** + * Baut den Namen des Formularfeldes, benutzerdefinierte Felder liegen verschachtelt + */ + fieldName: function(field) { + + if (field.custom) return 'wpsg[checkout][custom][' + field.custom + ']'; + + return 'wpsg[checkout][' + field.name + ']'; + + }, // fieldName(field) + + /** + * Hat der Server dieses Feld als fehlerhaft gemeldet? + */ + hasError: function(name) { + + return (this.state.errors.indexOf(name) >= 0); + + }, // hasError(name) + + /** + * Erhöht oder verringert die Menge einer Position + */ + changeAmount: function(product, diff) { + + var amount = parseInt(this.amounts[product.index], 10); + + if (isNaN(amount)) amount = product.amount; + + amount = amount + diff; + + if (amount < 1) amount = 1; + + this.amounts[product.index] = amount; + + this.refresh(); + + }, // changeAmount(product, diff) + + /** + * Schickt die Änderungen gesammelt an den Server + * + * Mehrere schnell aufeinander folgende Änderungen werden zu einer Anfrage + * zusammengefasst. + */ + refresh: function() { + + var self = this; + + if (this.timer !== null) window.clearTimeout(this.timer); + + this.timer = window.setTimeout(function() { + + self.timer = null; + self.send('update'); + + }, 250); + + }, // refresh() + + /** + * Entfernt eine Position aus dem Warenkorb + */ + removeProduct: function(product) { + + this.send('remove', { wpsg_produkt: product.index }); + + }, // removeProduct(product) + + /** + * Löst den eingegebenen Gutscheincode ein + */ + addVoucher: function() { + + if (this.voucher === '') return; + + this.send('voucherAdd'); + + }, // addVoucher() + + /** + * Nimmt einen eingelösten Gutschein wieder aus dem Warenkorb + */ + removeVoucher: function(item) { + + this.send('voucherRemove', { wpsg_voucher: item.id }); + + }, // removeVoucher(item) + + /** + * Schickt das Formular an den Server und übernimmt den neuen Zustand + * + * arKeepFiles: Namen von Dateifeldern, deren Datei mitgeschickt wird (Upload direkt nach der Auswahl) + */ + send: function(action, extra, arKeepFiles) { + + var el_form = document.getElementById('wpsg_smartcheckout_form'); + + if (el_form === null) return; + + // Dateien der direkt hochzuladenden Felder sofort sichern: der Block wird mit der nächsten Antwort neu aufgebaut + var arUpload = []; + + if (Array.isArray(arKeepFiles)) { + + arKeepFiles.forEach(function(name) { + + el_form.querySelectorAll('input[type="file"]').forEach(function(el) { + + if (el.name !== name || !el.files) return; + + for (var i = 0; i < el.files.length; i++) arUpload.push({ name: name, file: el.files[i] }); + + }); + + }); + + } + + // Es läuft bereits eine Anfrage, die Änderung wird danach nachgereicht (ein Upload samt seiner Dateien) + if (this.busy) { + + if (arUpload.length > 0) this.queuedUpload = { action: action, extra: extra, files: arUpload }; + else this.queued = true; + + return; + + } + + this.sendData(el_form, action, extra, arUpload); + + }, // send(action, extra, arKeepFiles) + + /** + * Baut die Formulardaten, schickt sie ab und übernimmt den neuen Zustand + * + * arUpload: [{ name, file }] Dateien, die mit dieser Anfrage hochgeladen werden + */ + sendData: function(el_form, action, extra, arUpload) { + + var self = this; + + var modules = this.captureModules(); + + var data = new FormData(el_form); + + // Dateien werden erst beim Abschluss übertragen, nicht bei jeder Aktualisierung, außer sie sollen direkt hochgeladen werden + el_form.querySelectorAll('input[type="file"][name]').forEach(function(el) { + + data.delete(el.name); + + }); + + (arUpload || []).forEach(function(item) { + + data.append(item.name, item.file, item.file.name); + + }); + + data.append('wpsg_smartcheckout_ajax', action); + + if (extra) { + + Object.keys(extra).forEach(function(key) { + + data.append(key, extra[key]); + + }); + + } + + this.busy = true; + + window.fetch(el_form.getAttribute('action'), { + method: 'POST', + body: data, + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }) + .then(function(response) { return response.json(); }) + .then(function(result) { + + self.busy = false; + + if (result.reload === true) { + + window.location.reload(); + + return; + + } + + self.voucherError = (result.voucherError === true); + + if (result.ok === true && action === 'voucherAdd') self.voucher = ''; + + if (result.state) { + + var accepted = self.state.accepted; + + self.state = result.state; + self.syncForm(false); + self.$nextTick(function() { + + self.restoreModules(modules); + self.revokeAccepted(accepted); + + }); + + } + + if (self.queuedUpload !== null) { + + var upload = self.queuedUpload; + + self.queuedUpload = null; + self.queued = false; + self.sendData(el_form, upload.action, upload.extra, upload.files); + + } else if (self.queued) { + + self.queued = false; + self.refresh(); + + } + + }) + .catch(function() { + + self.busy = false; + self.queued = false; + self.queuedUpload = null; + + }); + + }, // sendData(el_form, action, extra, arUpload) + + /** + * Sichert die Eingaben in den servergerenderten Modulblöcken + * + * Diese Blöcke werden vom Server neu geliefert, sobald sich der Warenkorb + * ändert — ein angekreuztes Kästchen wäre sonst nach jedem Versandartwechsel + * wieder leer. + */ + captureModules: function() { + + var data = {}; + + document.querySelectorAll('#wpsg_smartcheckout .sc_modul_dynamic [name]').forEach(function(el) { + + // Dateifelder lassen sich nicht per Script setzen, ein Zuweisen von value würde eine Exception werfen + if (el.type === 'file') return; + + if (el.type === 'checkbox' || el.type === 'radio') data[el.name + '\n' + el.value] = el.checked; + else data[el.name] = el.value; + + }); + + return data; + + }, // captureModules() + + /** + * Stellt die gesicherten Eingaben wieder her, soweit die Felder noch existieren + */ + restoreModules: function(data) { + + document.querySelectorAll('#wpsg_smartcheckout .sc_modul_dynamic [name]').forEach(function(el) { + + if (el.type === 'file') return; + + if (el.type === 'checkbox' || el.type === 'radio') { + + var key = el.name + '\n' + el.value; + + if (data.hasOwnProperty(key)) el.checked = data[key]; + + } else if (data.hasOwnProperty(el.name)) { + + el.value = data[el.name]; + + } + + }); + + }, // restoreModules(data) + + /** + * Entfernt die Haken bei AGB und Bestellbedingungen, deren Akzeptanz der Server zurückgenommen hat + * + * Das passiert, wenn sich die Bestellung nach der Rückkehr von einem Zahlungsanbieter ändert. + */ + revokeAccepted: function(before) { + + var after = this.state.accepted || { agb: false, ordercondition: [] }; + + if (!before) return; + + if (before.agb === true && after.agb !== true) this.agb = false; + + (before.ordercondition || []).forEach(function(oc_id) { + + if (after.ordercondition.indexOf(oc_id) >= 0) return; + + var el = document.querySelector('#wpsg_smartcheckout .sc_modul_dynamic input[name="wpsg_mod_ordercondition[' + oc_id + ']"]'); + + if (el !== null) el.checked = false; + + }); + + }, // revokeAccepted(before) + + /** + * Springt zum ersten beanstandeten Feld + */ + focusError: function() { + + var el_first = document.querySelector('#wpsg_smartcheckout .sc_invalid'); + + if (el_first === null) return; + + el_first.scrollIntoView({ behavior: 'smooth', block: 'center' }); + + var el_input = el_first.querySelector('input, select, textarea'); + + if (el_input !== null) el_input.focus(); + + }, // focusError() + + /** + * Blockt das Absenden, solange der Server noch rechnet + */ + /** + * Berechnet neu, wenn sich eine Eingabe in einem Block der Module ändert (z.B. Lieferadresse, Produktvariablen) + * + * Dateifelder laden über eigene Wege hoch, Zustimmungen ändern nichts an der Berechnung. + */ + onModuleChange: function(event) { + + var el = event.target; + + if (!el || typeof el.closest !== 'function') return; + + // Uploads starten direkt nach der Auswahl über den Button des Moduls (z.B. Upload zum Produkt) + if (el.type === 'file') { + + this.startUpload(el); + + return; + + } + + // Felder ohne Namen werden nicht übertragen, eine Aktualisierung brächte nichts + if (!el.name) return; + + if (el.closest('.sc_modul_dynamic, .sc_modul_extra, .sc_modul_customer') === null) return; + if (el.closest('.sc_modul_conditions') !== null) return; + + this.refresh(); + + }, // onModuleChange(event) + + /** + * Löst den Upload-Button des Moduls aus, sobald im Dateifeld daneben eine Datei gewählt wurde + * + * Der Button trägt data-sc-autoupload und wird im SmartCheckout ausgeblendet. Hochgeladen wird über + * den bestehenden Weg des Moduls, der auch das Ziel mit der Dateiliste neu befüllt. Trägt das Dateifeld + * selbst data-sc-upload, geht die Datei mit einer Aktualisierung des SmartCheckouts an den Server. + */ + startUpload: function(el_file) { + + if (!el_file.files || el_file.files.length === 0 || el_file.parentElement === null) return; + + // Dateifeld, das der SmartCheckout selbst an den Server schickt (z.B. Bestellupload) + if (el_file.hasAttribute('data-sc-upload') && el_file.name) { + + // Rückmeldung wie beim Upload zum Produkt, der Block wird mit der Antwort neu aufgebaut + if (el_file.hasAttribute('data-sc-upload-wait') && el_file.parentElement.querySelector('.sc_upload_wait') === null) { + + var el_wait = document.createElement('div'); + + el_wait.className = 'sc_upload_wait'; + el_wait.textContent = el_file.getAttribute('data-sc-upload-wait') + '…'; + + el_file.insertAdjacentElement('afterend', el_wait); + + } + + if (this.timer !== null) { + + window.clearTimeout(this.timer); + this.timer = null; + + } + + this.send('update', null, [el_file.name]); + + return; + + } + + var el_button = el_file.parentElement.querySelector('[data-sc-autoupload]'); + + if (el_button !== null) el_button.click(); + + }, // startUpload(el_file) + + /** + * Schickt die Menge eines Buttons mit data-sc-send unter dem dort angegebenen Namen an den Server + * + * Die Menge steht im Feld mit data-sc-amount innerhalb desselben data-sc-item, ein fester Wert in + * data-sc-value. Mit data-sc-confirm fragt der Button vorher nach. + */ + onModuleClick: function(event) { + + var el = event.target; + + if (!el || typeof el.closest !== 'function') return; + + var el_button = el.closest('.sc_modul_dynamic [data-sc-send]'); + + if (el_button === null) return; + + event.preventDefault(); + + if (this.busy) return; + + // Rückfrage des Moduls, z.B. vor dem Entfernen eines Dokuments + if (el_button.hasAttribute('data-sc-confirm') && !window.confirm(el_button.getAttribute('data-sc-confirm'))) return; + + var value; + + if (el_button.hasAttribute('data-sc-value')) { + + // Fester Wert des Buttons, z.B. der Index eines Dokuments + value = el_button.getAttribute('data-sc-value'); + + } else { + + var el_item = el_button.closest('[data-sc-item]'); + var el_amount = ((el_item !== null)?el_item.querySelector('[data-sc-amount]'):null); + + value = ((el_amount !== null)?parseInt(el_amount.value, 10):1); + + if (isNaN(value) || value < 1) value = 1; + + } + + var extra = {}; + extra[el_button.getAttribute('data-sc-send')] = value; + + // Eine noch ausstehende Änderung geht in derselben Anfrage mit + if (this.timer !== null) { + + window.clearTimeout(this.timer); + this.timer = null; + + } + + this.send('update', extra); + + }, // onModuleClick(event) + + /** + * Enter im Mengenfeld eines Modulblocks löst dessen Button aus, statt die Bestellung abzuschicken + */ + onModuleKeydown: function(event) { + + if (event.key !== 'Enter') return; + + var el = event.target; + + if (!el || typeof el.closest !== 'function' || !el.hasAttribute('data-sc-amount')) return; + + event.preventDefault(); + + var el_item = el.closest('[data-sc-item]'); + var el_button = ((el_item !== null)?el_item.querySelector('[data-sc-send]'):null); + + if (el_button !== null) el_button.click(); + + }, // onModuleKeydown(event) + + /** + * Blockt das Absenden, solange der Server noch rechnet oder eine Änderung noch nicht übertragen wurde + */ + onSubmit: function(event) { + + // Andere Buttons im Formular (z.B. Login der Kundenverwaltung) werden normal abgeschickt + if (event.submitter && event.submitter.name !== 'wpsg_smartcheckout_order') return; + + // Die Bestellung ist bereits unterwegs, ein weiterer Klick darf sie nicht ein zweites Mal abschicken + if (this.submitting) { + + event.preventDefault(); + + return; + + } + + if (this.timer !== null) { + + // Ausstehende Änderung sofort übertragen, der Kunde sieht danach die neue Summe + event.preventDefault(); + + window.clearTimeout(this.timer); + this.timer = null; + this.send('update'); + + return; + + } + + if (this.busy) { + + event.preventDefault(); + + return; + + } + + // Das GARAN-Gewährleistungslabel muss vor dem Bestellabschluss zur Kenntnis genommen werden + if (this.state.warranty && this.state.warranty.popup === true && this.garanConfirmed !== true) { + + event.preventDefault(); + + this.garanSubmit = true; + this.garanPopup = true; + + return; + + } + + // Ab hier wird tatsächlich bestellt: Layer über die Seite, bis der Browser die nächste Seite lädt + this.submitting = true; + + }, // onSubmit(event) + + /** + * Schließt das GARAN-Popup, kam es über den Bestellbutton, wird die Bestellung danach abgeschickt + */ + confirmGaran: function() { + + this.garanConfirmed = true; + this.garanPopup = false; + + if (this.garanSubmit !== true) return; + + this.garanSubmit = false; + + var el_form = document.getElementById('wpsg_smartcheckout_form'); + var el_button = el_form.querySelector('button[name="wpsg_smartcheckout_order"]'); + + // requestSubmit() übermittelt den Bestellbutton mit und prüft die Pflichtfelder erneut + if (typeof el_form.requestSubmit === 'function' && el_button !== null) el_form.requestSubmit(el_button); + else if (el_button !== null) el_button.click(); + + } // confirmGaran() + + } + + }).mount('#wpsg_smartcheckout'); + +})(); diff --git a/views/mods/mod_basketteaser/smartcheckout.phtml b/views/mods/mod_basketteaser/smartcheckout.phtml new file mode 100644 index 0000000..712098c --- /dev/null +++ b/views/mods/mod_basketteaser/smartcheckout.phtml @@ -0,0 +1,44 @@ +<?php + + /** + * Template für die Warenkorbteaserprodukte im SmartCheckout + * + * Das Mengenfeld hat bewusst keinen Namen: es wird so bei den Aktualisierungen des + * Checkouts nicht mitgeschickt. Erst der Button überträgt die Menge unter + * wpsg_mod_basketteaser_row[ID] (siehe onModuleClick in smartcheckout.js). + */ + +?> +<div class="sc_teaser"> + + <div class="sc_teaser_title"><?php echo esc_html__('Das könnte Sie auch interessieren', 'wpsg'); ?></div> + + <?php foreach ($this->view['wpsg_mod_basketteaser']['products'] as $p) { ?> + <div class="sc_teaser_item" data-sc-item> + + <?php if ($p['image'] !== '') { ?> + <div class="sc_teaser_image"> + <img src="<?php echo esc_url($p['image']); ?>" alt="<?php echo esc_attr($p['name']); ?>" loading="lazy" /> + </div> + <?php } ?> + + <div class="sc_teaser_body"> + <div class="sc_teaser_name"> + <?php if ($p['link'] !== '') { ?> + <a href="<?php echo esc_url($p['link']); ?>"><?php echo esc_html($p['name']); ?></a> + <?php } else { ?> + <?php echo esc_html($p['name']); ?> + <?php } ?> + </div> + <div class="sc_teaser_price"><?php echo esc_html($p['price']); ?></div> + </div> + + <div class="sc_teaser_action"> + <input type="text" inputmode="numeric" class="sc_teaser_amount" value="1" data-sc-amount aria-label="<?php echo esc_attr__('Menge', 'wpsg'); ?>" /> + <button type="button" class="sc_button sc_button_secondary" data-sc-send="wpsg_mod_basketteaser_row[<?php echo intval($p['id']); ?>]"><?php echo esc_html__('Hinzufügen', 'wpsg'); ?></button> + </div> + + </div> + <?php } ?> + +</div> diff --git a/views/mods/mod_orderupload/smartcheckout_checkout.phtml b/views/mods/mod_orderupload/smartcheckout_checkout.phtml new file mode 100644 index 0000000..7da51cb --- /dev/null +++ b/views/mods/mod_orderupload/smartcheckout_checkout.phtml @@ -0,0 +1,77 @@ +<?php + + /** + * Template für den Bestellupload im SmartCheckout + * + * Aufbau und Verhalten wie beim Upload zum Produkt (smartcheckout_row.phtml): Dateiliste mit Größe und + * Löschen, darunter die Dateiauswahl. Die Datei wird direkt nach der Auswahl hochgeladen (data-sc-upload, + * siehe smartcheckout.js), entfernt wird über data-sc-send, beides landet in + * wpsg_mod_orderupload::smartcheckout_ajax(). Der Block wird bei jeder Antwort neu geliefert. + */ + + $arUploads = (array)($_SESSION['wpsg']['wpsg_mod_orderupload'] ?? []); + $uploadsum = floatval($this->view['wpsg_mod_orderupload']['uploadsum'] ?? 0); + $maxweight = (string)($this->view['wpsg_mod_orderupload']['maxweight'] ?? ''); + $bRequired = ($this->get_option('wpsg_mod_orderupload_optional') != '1'); + $bMulti = ($this->get_option('wpsg_mod_orderupload_multifiles') == '1'); + $arMessages = (array)($this->view['wpsg_mod_orderupload']['messages'] ?? []); + $arAllowed = (array)($this->view['wpsg_mod_orderupload']['allowedext'] ?? []); + $strAccept = implode(',', array_map(function($ext) { return '.'.$ext; }, $arAllowed)); + +?> +<div class="sc_orderupload"> + + <h2 class="sc_title"><?php echo esc_html__('Dateiupload', 'wpsg'); ?><?php if ($bRequired) { ?><span class="sc_required" aria-hidden="true">*</span><?php } ?></h2> + + <div class="sc_upload"> + + <div class="sc_upload_label"><?php echo esc_html__('Upload zur Bestellung', 'wpsg'); ?></div> + + <div class="sc_upload_target"> + + <?php /* Meldungen des letzten Uploads bzw. Entfernens, wie beim Upload zum Produkt direkt am Block */ ?> + <?php foreach ($arMessages as $message) { ?> + <div class="sc_upload_message sc_upload_message_<?php echo esc_attr($message['type']); ?>"><?php echo wp_kses_post($message['text']); ?></div> + <?php } ?> + + <?php if (wpsg_isSizedArray($arUploads)) { ?> + <ul> + <?php foreach ($arUploads as $ou_index => $ou) { ?> + <li> + <a href="<?php echo esc_url($ou[2]); ?>" target="_blank" rel="noopener"><?php echo wpsg_hspc($ou[0]); ?> <span class="size">(<?php echo esc_html(wpsg_formatSize($ou[3])); ?>)</span></a> + <a + href="#" + class="removeLink" + role="button" + data-sc-send="wpsg_mod_orderupload_remove" + data-sc-value="<?php echo intval($ou_index); ?>" + data-sc-confirm="<?php echo esc_attr__('Sind Sie sich sicher, dass Sie dieses Dokument aus der Bestellung entfernen möchten?', 'wpsg'); ?>" + title="<?php echo esc_attr__('Upload entfernen.', 'wpsg'); ?>" + ><img src="<?php echo esc_url(WPSG_URL.'views/gfx/bin_x.png'); ?>" alt="<?php echo esc_attr__('Löschen', 'wpsg'); ?>" /></a> + </li> + <?php } ?> + </ul> + <?php } ?> + + <?php if ($uploadsum > 0) { ?> + <input + type="file" + name="wpsg_mod_orderupload_file<?php echo (($bMulti)?'[]':''); ?>" + data-sc-upload="1" + data-sc-upload-wait="<?php echo esc_attr__('Bitte warten ', 'wpsg'); ?>" + <?php if ($strAccept !== '') { ?>accept="<?php echo esc_attr($strAccept); ?>"<?php } ?> + <?php echo (($bMulti)?'multiple="multiple"':''); ?> + aria-label="<?php echo esc_attr__('Upload zur Bestellung', 'wpsg'); ?>" + /> + <?php } ?> + + </div> + + <div class="sc_hint"><?php echo esc_html(wpsg_translate(__('Es stehen Ihnen #1# Mb von #2# Mb für den Upload zur Verfügung', 'wpsg'), $uploadsum, $maxweight)); ?></div> + <?php if (wpsg_isSizedArray($arAllowed)) { ?> + <div class="sc_hint"><?php echo esc_html(wpsg_translate(__('Erlaubte Dateiendungen: #1#', 'wpsg'), implode(', ', $arAllowed))); ?></div> + <?php } ?> + + </div> + +</div> diff --git a/views/mods/mod_orderupload/smartcheckout_row.phtml b/views/mods/mod_orderupload/smartcheckout_row.phtml new file mode 100644 index 0000000..66986f9 --- /dev/null +++ b/views/mods/mod_orderupload/smartcheckout_row.phtml @@ -0,0 +1,21 @@ +<?php + + /** + * Template für den Upload zum Produkt an einer Warenkorbposition im SmartCheckout + * + * Das Ziel trägt dieselbe ID wie im klassischen Warenkorb, die Scripte aus basket_after.phtml + * ersetzen seinen Inhalt nach dem Hochladen bzw. Löschen. + */ + + $product_index = $this->view['wpsg_mod_orderupload']['product_index']; + +?> +<div class="sc_upload"> + + <div class="sc_upload_label"><?php echo esc_html__('Upload zum Produkt', 'wpsg'); ?></div> + + <div id="wpsg_mod_orderupload_<?php echo intval($product_index); ?>_target" class="wpsg_mod_orderupload_target sc_upload_target"> + <?php echo $this->view['wpsg_mod_orderupload']['strHTMLTarget']; ?> + </div> + +</div> diff --git a/views/mods/mod_paypalapi/overview_preauth_button.phtml b/views/mods/mod_paypalapi/overview_preauth_button.phtml new file mode 100644 index 0000000..9fd775a --- /dev/null +++ b/views/mods/mod_paypalapi/overview_preauth_button.phtml @@ -0,0 +1,15 @@ +<?php + + /** + * Ersetzt in der Bestellübersicht den Button "zahlungspflichtig bestellen", solange die Zahlung noch nicht bei PayPal autorisiert wurde + */ + +?> + +<?php if (($this->view['wpsg_mod_paypalapi']['preauth_changed']??false) === true) { ?> +<div class="wpsg_mod_paypalapi_preauth_hint"> + <p><?php echo esc_html__('Ihre Bestellung hat sich seit der Autorisierung geändert. Bitte autorisieren Sie die Zahlung erneut bei PayPal.', 'wpsg'); ?></p> +</div> +<?php } ?> + +<input type="submit" class="wpsg_orderbutton wpsg_mod_paypalapi_preauth_button" value="<?php echo esc_attr__('Mit PayPal bezahlen', 'wpsg'); ?>" name="wpsg_order" /> diff --git a/views/mods/mod_productvars/smartcheckout_row.phtml b/views/mods/mod_productvars/smartcheckout_row.phtml new file mode 100644 index 0000000..b5845b5 --- /dev/null +++ b/views/mods/mod_productvars/smartcheckout_row.phtml @@ -0,0 +1,57 @@ +<?php + + /** + * Template für die Produktvariablen an einer Warenkorbposition im SmartCheckout + * + * Die Feldnamen entsprechen denen aus basket_row.phtml, gespeichert wird über + * wpsg_mod_productvars::basket_preUpdate(). Pflichtfelder tragen required, der leere + * Platzhalter der Auswahl wird serverseitig zu not_set. + */ + + $p = $this->view['wpsg_mod_productvars']['p']; + $product_index = $p['product_index']; + $arError = (array)$this->view['error']; + +?> +<div class="sc_pvars"> + + <?php foreach ($this->view['wpsg_mod_productvars']['data'] as $pv) { ?> + <?php + + $field_name = 'wpsg_mod_productvars['.$pv['id'].']['.$product_index.']'; + $field_id = 'wpsg_sc_pvar_'.$pv['id'].'_'.$product_index; + $value = wpsg_getStr($p['wpsg_mod_productvars'][$pv['id']]); + $bError = in_array('wpsg_mod_productvars_'.$pv['id'].'_'.$product_index, $arError); + $bRequired = ($pv['pflicht'] == '1'); + $strRequired = (($bRequired)?'<span class="sc_required" aria-hidden="true">*</span>':''); + + ?> + <div class="sc_pvar sc_pvar_typ<?php echo intval($pv['typ']); ?><?php echo (($bError)?' sc_invalid':''); ?>"> + + <?php if ($pv['typ'] == '3') { // Checkbox ?> + <label class="sc_pvar_check"> + <input type="hidden" value="0" name="<?php echo esc_attr($field_name); ?>" /> + <input type="checkbox" value="1" name="<?php echo esc_attr($field_name); ?>" <?php echo (($value === '1')?'checked="checked"':''); ?><?php echo (($bRequired)?' required':''); ?> /> + <span><?php echo wpsg_hspc(__($pv['name'], 'wpsg')); ?><?php echo $strRequired; ?></span> + </label> + <?php } else { ?> + <label class="sc_pvar_label" for="<?php echo esc_attr($field_id); ?>"><?php echo wpsg_hspc(__($pv['name'], 'wpsg')); ?><?php echo $strRequired; ?></label> + + <?php if ($pv['typ'] == '1') { // Auswahlfeld ?> + <select class="sc_pvar_input" id="<?php echo esc_attr($field_id); ?>" name="<?php echo esc_attr($field_name); ?>"<?php echo (($bRequired)?' required':''); ?>> + <option value=""><?php echo __('Bitte wählen', 'wpsg'); ?></option> + <?php foreach ((array)$pv['auswahl'] as $option) { ?> + <option <?php echo ((wpsg_hspc($value) === wpsg_hspc($option))?'selected="selected"':''); ?> value="<?php echo wpsg_hspc($option); ?>"><?php echo wpsg_hspc($option); ?></option> + <?php } ?> + </select> + <?php } else if ($pv['typ'] == '4') { // Textfeld mehrzeilig ?> + <textarea class="sc_pvar_input" id="<?php echo esc_attr($field_id); ?>" name="<?php echo esc_attr($field_name); ?>" rows="3"<?php echo (($bRequired)?' required':''); ?>><?php echo wpsg_hspc($value); ?></textarea> + <?php } else { // Textfeld ?> + <input class="sc_pvar_input" type="text" id="<?php echo esc_attr($field_id); ?>" name="<?php echo esc_attr($field_name); ?>" value="<?php echo wpsg_hspc($value); ?>"<?php echo (($bRequired)?' required':''); ?> /> + <?php } ?> + <?php } ?> + + </div> + <?php } ?> + +</div> |
