Add payment details component
This commit is contained in:
parent
f87ba31a0c
commit
1105cc947f
25
package-lock.json
generated
25
package-lock.json
generated
@ -25,6 +25,7 @@
|
|||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"@types/simplemde": "^1.11.11",
|
"@types/simplemde": "^1.11.11",
|
||||||
"@types/wcag-contrast": "^3.0.3",
|
"@types/wcag-contrast": "^3.0.3",
|
||||||
"a11y.css": "^5.3.0",
|
"a11y.css": "^5.3.0",
|
||||||
@ -49,6 +50,7 @@
|
|||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"object-path": "^0.11.8",
|
"object-path": "^0.11.8",
|
||||||
|
"qrcode-svg": "^1.1.0",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"sealious": "^0.21.30",
|
"sealious": "^0.21.30",
|
||||||
"slug": "^9.1.0",
|
"slug": "^9.1.0",
|
||||||
@ -1962,6 +1964,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
||||||
"integrity": "sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw=="
|
"integrity": "sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/qrcode-svg": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode-svg/-/qrcode-svg-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-GjkD+HB8S1wrIsf3skHDtcYBjzNhTxocMbX+wG166xDkaVOnLiMUla7bLjbwxo6mMvqqWQNP0Dk8nkIeizSmnw=="
|
||||||
|
},
|
||||||
"node_modules/@types/qs": {
|
"node_modules/@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
@ -11369,6 +11376,14 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/qrcode-svg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw==",
|
||||||
|
"bin": {
|
||||||
|
"qrcode-svg": "bin/qrcode-svg.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/qs": {
|
"node_modules/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
@ -16562,6 +16577,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
||||||
"integrity": "sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw=="
|
"integrity": "sha512-4tgJ1Z3elF/tOMpA8JLVuR9spt9Ynsf7+JjqsQ2IqtiPJtcLoHoXcT6qU4E10cPFqyXX5HDm9QwIzZhBSkLxsw=="
|
||||||
},
|
},
|
||||||
|
"@types/qrcode-svg": {
|
||||||
|
"version": "1.1.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/qrcode-svg/-/qrcode-svg-1.1.5.tgz",
|
||||||
|
"integrity": "sha512-GjkD+HB8S1wrIsf3skHDtcYBjzNhTxocMbX+wG166xDkaVOnLiMUla7bLjbwxo6mMvqqWQNP0Dk8nkIeizSmnw=="
|
||||||
|
},
|
||||||
"@types/qs": {
|
"@types/qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||||
@ -23520,6 +23540,11 @@
|
|||||||
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
|
||||||
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="
|
||||||
},
|
},
|
||||||
|
"qrcode-svg": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/qrcode-svg/-/qrcode-svg-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-XyQCIXux1zEIA3NPb0AeR8UMYvXZzWEhgdBgBjH9gO7M48H9uoHzviNz8pXw3UzrAcxRRRn9gxHewAVK7bn9qw=="
|
||||||
|
},
|
||||||
"qs": {
|
"qs": {
|
||||||
"version": "6.14.0",
|
"version": "6.14.0",
|
||||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||||
|
|||||||
@ -108,6 +108,7 @@
|
|||||||
"@types/kill-port": "^2.0.0",
|
"@types/kill-port": "^2.0.0",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
|
"@types/qrcode-svg": "^1.1.5",
|
||||||
"@types/simplemde": "^1.11.11",
|
"@types/simplemde": "^1.11.11",
|
||||||
"@types/wcag-contrast": "^3.0.3",
|
"@types/wcag-contrast": "^3.0.3",
|
||||||
"a11y.css": "^5.3.0",
|
"a11y.css": "^5.3.0",
|
||||||
@ -132,6 +133,7 @@
|
|||||||
"normalize-diacritics": "^5.0.0",
|
"normalize-diacritics": "^5.0.0",
|
||||||
"nyc": "^17.1.0",
|
"nyc": "^17.1.0",
|
||||||
"object-path": "^0.11.8",
|
"object-path": "^0.11.8",
|
||||||
|
"qrcode-svg": "^1.1.0",
|
||||||
"qs": "^6.12.0",
|
"qs": "^6.12.0",
|
||||||
"sealious": "^0.21.30",
|
"sealious": "^0.21.30",
|
||||||
"slug": "^9.1.0",
|
"slug": "^9.1.0",
|
||||||
|
|||||||
57
src/back/jdd-components/payment-details/payment-details.css
Normal file
57
src/back/jdd-components/payment-details/payment-details.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.payment-details {
|
||||||
|
display: grid;
|
||||||
|
gap: 24px;
|
||||||
|
grid-template-columns: 2fr 1fr;
|
||||||
|
|
||||||
|
@container (max-width: 580px) {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left,
|
||||||
|
.right {
|
||||||
|
background-color: var(--color-brand-canvas);
|
||||||
|
box-shadow: 4px 4px #00000026;
|
||||||
|
padding: 16px;
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
display: flex;
|
||||||
|
flex-flow: column;
|
||||||
|
gap: 24px;
|
||||||
|
|
||||||
|
&:first-child() {
|
||||||
|
flex-grow: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-section {
|
||||||
|
label {
|
||||||
|
color: var(--color-brand-accent);
|
||||||
|
text-transform: uppercase;
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 16px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
svg {
|
||||||
|
height: 16px;
|
||||||
|
width: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-value {
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,84 @@
|
|||||||
|
import { TempstreamJSX } from "tempstream";
|
||||||
|
import type { ComponentToHTMLArgs } from "@sealcode/jdd";
|
||||||
|
import { Component, ComponentArguments, insert_nbsp } from "@sealcode/jdd";
|
||||||
|
import { icon } from "src/back/icons.js";
|
||||||
|
import generateQRCached from "src/back/qr.js";
|
||||||
|
|
||||||
|
const component_arguments = {
|
||||||
|
title: new ComponentArguments.ShortText(),
|
||||||
|
recipient: new ComponentArguments.ShortText(),
|
||||||
|
account_no: new ComponentArguments.ShortText(),
|
||||||
|
payment_title: new ComponentArguments.ShortText(),
|
||||||
|
qr_title: new ComponentArguments.ShortText(),
|
||||||
|
qr_subtitle: new ComponentArguments.ShortText(),
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export class PaymentDetails extends Component<typeof component_arguments> {
|
||||||
|
getArguments() {
|
||||||
|
return component_arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
async toHTML({
|
||||||
|
args: { title, recipient, account_no, payment_title, qr_title, qr_subtitle },
|
||||||
|
classes,
|
||||||
|
}: ComponentToHTMLArgs<typeof component_arguments>): Promise<string> {
|
||||||
|
return (
|
||||||
|
<div class={["payment-details", ...classes]}>
|
||||||
|
<div class="left">
|
||||||
|
<h2>{insert_nbsp(title)}</h2>
|
||||||
|
<div class="copy-section" data-controller="copy">
|
||||||
|
<label>
|
||||||
|
Odbiorca{" "}
|
||||||
|
<button
|
||||||
|
data-action="click->copy#copy"
|
||||||
|
data-copy-target="button"
|
||||||
|
>
|
||||||
|
{icon("copy")}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<div class="copy-value" data-copy-target="content">
|
||||||
|
{recipient}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="copy-section" data-controller="copy">
|
||||||
|
<label>
|
||||||
|
Numer rachunku{" "}
|
||||||
|
<button
|
||||||
|
data-action="click->copy#copy"
|
||||||
|
data-copy-target="button"
|
||||||
|
>
|
||||||
|
{icon("copy")}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<div class="copy-value" data-copy-target="content">
|
||||||
|
{account_no}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="copy-section" data-controller="copy">
|
||||||
|
<label>
|
||||||
|
Tytuł przelewu{" "}
|
||||||
|
<button
|
||||||
|
data-action="click->copy#copy"
|
||||||
|
data-copy-target="button"
|
||||||
|
>
|
||||||
|
{icon("copy")}
|
||||||
|
</button>
|
||||||
|
</label>
|
||||||
|
<div class="copy-value" data-copy-target="content">
|
||||||
|
{payment_title}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="right">
|
||||||
|
<h2>{qr_title}</h2>
|
||||||
|
<div class="qr">
|
||||||
|
{generateQRCached(
|
||||||
|
`|PL|${account_no.replaceAll(/[^0-9]/g, "")}|010000|${recipient.trim()}|${payment_title.trim()}|||`
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<p>{qr_subtitle}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/back/qr.ts
Normal file
10
src/back/qr.ts
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
import { default as qrcode } from "qrcode-svg";
|
||||||
|
|
||||||
|
const qr_cache: Record<string, string> = {};
|
||||||
|
|
||||||
|
export default function generateQRCached(text: string) {
|
||||||
|
if (!qr_cache[text]) {
|
||||||
|
qr_cache[text] = new qrcode(text).svg();
|
||||||
|
}
|
||||||
|
return qr_cache[text];
|
||||||
|
}
|
||||||
@ -16,6 +16,9 @@ application.register("map-with-pins", MapWithPins);
|
|||||||
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
|
import { default as HorizontalScroller } from "./../back/routes/common/horizontal-scroller/horizontal-scroller.stimulus.js";
|
||||||
application.register("horizontal-scroller", HorizontalScroller);
|
application.register("horizontal-scroller", HorizontalScroller);
|
||||||
|
|
||||||
|
import { default as Copy } from "./controllers/copy.stimulus.js";
|
||||||
|
application.register("copy", Copy);
|
||||||
|
|
||||||
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
|
import { default as AutogrowTextarea } from "./../../node_modules/@sealcode/jdd-editor/src/controllers/autogrow-textarea.stimulus.js";
|
||||||
application.register("autogrow-textarea", AutogrowTextarea);
|
application.register("autogrow-textarea", AutogrowTextarea);
|
||||||
|
|
||||||
|
|||||||
85
src/front/controllers/copy.stimulus.ts
Normal file
85
src/front/controllers/copy.stimulus.ts
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-unsafe-member-access */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { Controller } from "@hotwired/stimulus";
|
||||||
|
|
||||||
|
export default class CopyController extends Controller {
|
||||||
|
static targets = ["content", "button"];
|
||||||
|
declare contentTarget: HTMLElement;
|
||||||
|
declare buttonTarget: HTMLButtonElement;
|
||||||
|
|
||||||
|
private toast: HTMLDivElement | null = null;
|
||||||
|
|
||||||
|
private anchorName = `--copy-button-${crypto.randomUUID()}`;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
// Anchor the toast to the copy button.
|
||||||
|
(this.buttonTarget.style as any).anchorName = this.anchorName;
|
||||||
|
}
|
||||||
|
|
||||||
|
async copy() {
|
||||||
|
const value = this.contentTarget.textContent.trim();
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(value);
|
||||||
|
this.showToast();
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
this.showToast("Nie udało się skopiować");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private showToast(message = "Skopiowano!") {
|
||||||
|
this.removeToast();
|
||||||
|
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
|
||||||
|
Object.assign(toast.style, {
|
||||||
|
position: "fixed",
|
||||||
|
positionAnchor: this.anchorName,
|
||||||
|
positionArea: "right",
|
||||||
|
marginLeft: "8px",
|
||||||
|
|
||||||
|
padding: "5px 8px",
|
||||||
|
borderRadius: "6px",
|
||||||
|
background: "#222",
|
||||||
|
color: "#fff",
|
||||||
|
fontSize: "12px",
|
||||||
|
lineHeight: "1",
|
||||||
|
whiteSpace: "nowrap",
|
||||||
|
|
||||||
|
opacity: "0",
|
||||||
|
transform: "translateX(-4px)",
|
||||||
|
transition: "opacity 120ms ease, transform 120ms ease",
|
||||||
|
|
||||||
|
pointerEvents: "none",
|
||||||
|
zIndex: "1000",
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.textContent = message;
|
||||||
|
|
||||||
|
this.element.appendChild(toast);
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
toast.style.opacity = "1";
|
||||||
|
toast.style.transform = "translateX(0)";
|
||||||
|
});
|
||||||
|
|
||||||
|
this.toast = toast;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
toast.style.opacity = "0";
|
||||||
|
toast.style.transform = "translateX(-4px)";
|
||||||
|
|
||||||
|
toast.addEventListener("transitionend", () => toast.remove(), {
|
||||||
|
once: true,
|
||||||
|
});
|
||||||
|
}, 1500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeToast() {
|
||||||
|
this.toast?.remove();
|
||||||
|
this.toast = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
41
src/icons/copy.svg
Normal file
41
src/icons/copy.svg
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<svg
|
||||||
|
width="32"
|
||||||
|
height="32"
|
||||||
|
fill="currentColor"
|
||||||
|
class="bi bi-copy"
|
||||||
|
viewBox="0 0 32 32"
|
||||||
|
version="1.1"
|
||||||
|
id="svg1"
|
||||||
|
sodipodi:docname="copy.svg"
|
||||||
|
inkscape:version="1.4.4 (dcaf3e7d9e, 2026-05-05)"
|
||||||
|
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||||
|
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:svg="http://www.w3.org/2000/svg">
|
||||||
|
<defs
|
||||||
|
id="defs1" />
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="namedview1"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#000000"
|
||||||
|
borderopacity="0.25"
|
||||||
|
inkscape:showpageshadow="2"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pagecheckerboard="0"
|
||||||
|
inkscape:deskcolor="#d1d1d1"
|
||||||
|
inkscape:zoom="17.699693"
|
||||||
|
inkscape:cx="13.361814"
|
||||||
|
inkscape:cy="13.983294"
|
||||||
|
inkscape:window-width="1920"
|
||||||
|
inkscape:window-height="1048"
|
||||||
|
inkscape:window-x="0"
|
||||||
|
inkscape:window-y="0"
|
||||||
|
inkscape:window-maximized="1"
|
||||||
|
inkscape:current-layer="svg1" />
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="m 8,4 a 4,4 0 0 1 4,-4 h 16 a 4,4 0 0 1 4,4 v 16 a 4,4 0 0 1 -4,4 H 12 A 4,4 0 0 1 8,20 Z m 4,-2 a 2,2 0 0 0 -2,2 v 16 a 2,2 0 0 0 2,2 h 16 a 2,2 0 0 0 2,-2 V 4 A 2,2 0 0 0 28,2 Z m -8,8 a 2,2 0 0 0 -2,2 v 16 a 2,2 0 0 0 2,2 h 16 a 2,2 0 0 0 2,-2 v -2 h 2 v 2 a 4,4 0 0 1 -4,4 H 4 A 4,4 0 0 1 0,28 V 12 A 4,4 0 0 1 4,8 h 2 v 2 z"
|
||||||
|
id="path1"
|
||||||
|
style="stroke-width:1.99999" />
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.5 KiB |
Loading…
x
Reference in New Issue
Block a user