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/leaflet": "^1.9.8",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"@types/simplemde": "^1.11.11",
|
||||
"@types/wcag-contrast": "^3.0.3",
|
||||
"a11y.css": "^5.3.0",
|
||||
@ -49,6 +50,7 @@
|
||||
"normalize-diacritics": "^5.0.0",
|
||||
"nyc": "^17.1.0",
|
||||
"object-path": "^0.11.8",
|
||||
"qrcode-svg": "^1.1.0",
|
||||
"qs": "^6.12.0",
|
||||
"sealious": "^0.21.30",
|
||||
"slug": "^9.1.0",
|
||||
@ -1962,6 +1964,11 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/object-path/-/object-path-0.11.4.tgz",
|
||||
"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": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
|
||||
@ -11369,6 +11376,14 @@
|
||||
"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": {
|
||||
"version": "6.14.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "6.14.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "6.14.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz",
|
||||
|
||||
@ -108,6 +108,7 @@
|
||||
"@types/kill-port": "^2.0.0",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/qrcode-svg": "^1.1.5",
|
||||
"@types/simplemde": "^1.11.11",
|
||||
"@types/wcag-contrast": "^3.0.3",
|
||||
"a11y.css": "^5.3.0",
|
||||
@ -132,6 +133,7 @@
|
||||
"normalize-diacritics": "^5.0.0",
|
||||
"nyc": "^17.1.0",
|
||||
"object-path": "^0.11.8",
|
||||
"qrcode-svg": "^1.1.0",
|
||||
"qs": "^6.12.0",
|
||||
"sealious": "^0.21.30",
|
||||
"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";
|
||||
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";
|
||||
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