diff --git a/apps/ui/app/checkout/page.tsx b/apps/ui/app/checkout/page.tsx new file mode 100644 index 000000000..4de6d42df --- /dev/null +++ b/apps/ui/app/checkout/page.tsx @@ -0,0 +1,649 @@ +"use client"; + +import { + BanknoteIcon, + CreditCardIcon, + LockIcon, + ShieldCheckIcon, + SmartphoneIcon, +} from "lucide-react"; +import { type FormEvent, useState } from "react"; + +import { Badge } from "@/registry/default/ui/badge"; +import { Button } from "@/registry/default/ui/button"; +import { + Card, + CardDescription, + CardHeader, + CardPanel, + CardTitle, +} from "@/registry/default/ui/card"; +import { Field, FieldError, FieldLabel } from "@/registry/default/ui/field"; +import { Form } from "@/registry/default/ui/form"; +import { Input } from "@/registry/default/ui/input"; +import { Label } from "@/registry/default/ui/label"; +import { + Select, + SelectItem, + SelectPopup, + SelectTrigger, + SelectValue, +} from "@/registry/default/ui/select"; +import { Separator } from "@/registry/default/ui/separator"; +import { Spinner } from "@/registry/default/ui/spinner"; +import { Tabs, TabsList, TabsPanel, TabsTab } from "@/registry/default/ui/tabs"; + +const countryOptions = [ + { label: "United States", value: "us" }, + { label: "Canada", value: "ca" }, + { label: "United Kingdom", value: "uk" }, + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Australia", value: "au" }, +]; + +const sepaCountryOptions = [ + { label: "Germany", value: "de" }, + { label: "France", value: "fr" }, + { label: "Netherlands", value: "nl" }, + { label: "Belgium", value: "be" }, + { label: "Austria", value: "at" }, + { label: "Spain", value: "es" }, + { label: "Italy", value: "it" }, +]; + +const orderItems = [ + { id: "pro-plan", name: "Pro Plan (Annual)", price: 199.0, quantity: 1 }, + { id: "seats", name: "Additional Seats (5)", price: 49.0, quantity: 1 }, +]; + +function ApplePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +function GooglePayIcon({ className }: { className?: string }) { + return ( + + ); +} + +export default function CheckoutPage() { + const [loading, setLoading] = useState(false); + const [cardNumber, setCardNumber] = useState(""); + const [expiry, setExpiry] = useState(""); + const [cvc, setCvc] = useState(""); + const [iban, setIban] = useState(""); + + const subtotal = orderItems.reduce( + (sum, item) => sum + item.price * item.quantity, + 0, + ); + const tax = subtotal * 0.1; + const total = subtotal + tax; + + const formatCardNumber = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + const matches = v.match(/\d{4,16}/g); + const match = matches?.[0] || ""; + const parts = []; + for (let i = 0, len = match.length; i < len; i += 4) { + parts.push(match.substring(i, i + 4)); + } + if (parts.length) { + return parts.join(" "); + } + return value; + }; + + const formatExpiry = (value: string) => { + const v = value.replace(/\s+/g, "").replace(/[^0-9]/gi, ""); + if (v.length >= 2) { + return `${v.substring(0, 2)}/${v.substring(2, 4)}`; + } + return v; + }; + + const formatIban = (value: string) => { + const v = value.replace(/\s+/g, "").toUpperCase(); + const parts = []; + for (let i = 0, len = v.length; i < len; i += 4) { + parts.push(v.substring(i, i + 4)); + } + return parts.join(" "); + }; + + const onSubmit = async (e: FormEvent) => { + e.preventDefault(); + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert("Payment successful! Thank you for your purchase."); + }; + + const handleWalletPayment = async (wallet: string) => { + setLoading(true); + await new Promise((r) => setTimeout(r, 2000)); + setLoading(false); + alert(`${wallet} payment successful! Thank you for your purchase.`); + }; + + return ( +
+
+ + +
+
+ + Choose your preferred payment method + +
+ + + + + + + + + + + + +
+
+
+ +
+ + First Name + + Please enter your first name. + + + Last Name + + Please enter your last name. + +
+ + Email + + Please enter a valid email. + +
+ + + +
+ + + Card Number + + setCardNumber(formatCardNumber(e.target.value)) + } + placeholder="4242 4242 4242 4242" + required + type="text" + value={cardNumber} + /> + + Please enter a valid card number. + + +
+ + Expiration Date + + setExpiry(formatExpiry(e.target.value)) + } + placeholder="MM/YY" + required + type="text" + value={expiry} + /> + + Please enter a valid expiry date. + + + + CVC + + setCvc(e.target.value.replace(/[^0-9]/g, "")) + } + placeholder="123" + required + type="text" + value={cvc} + /> + Please enter a valid CVC. + +
+
+ + + +
+ + + Country + + + + Street Address + + Please enter your address. + +
+ + City + + Please enter your city. + + + State + + Please enter your state. + + + ZIP Code + + Please enter your ZIP code. + +
+
+
+
+ +
+
+
+
+
+ + +
+
+ +

+ Pay quickly and securely with your preferred digital + wallet. +

+
+ +
+ + + +
+ + + +
+
+
+
+ +
+
+
+
+ + +
+
+
+ +

+ Pay directly from your European bank account. Available + for customers in the Single Euro Payments Area. +

+
+ +
+ + + Full Name + + + Please enter the account holder name. + + + + Email + + Please enter a valid email. + +
+ + + +
+ + + IBAN + setIban(formatIban(e.target.value))} + placeholder="DE89 3704 0044 0532 0130 00" + required + type="text" + value={iban} + /> + Please enter a valid IBAN. + + + Country + + +
+ +
+

+ By providing your IBAN and confirming this payment, you + authorize (A) this company and Stripe, our payment + service provider, to send instructions to your bank to + debit your account and (B) your bank to debit your + account in accordance with those instructions. You are + entitled to a refund from your bank under the terms and + conditions of your agreement with your bank. +

+
+
+
+ +
+
+
+
+
+
+
+
+ +
+ + + Order Summary + + +
+ {orderItems.map((item) => ( +
+
+ {item.name} + + Qty: {item.quantity} + +
+ + ${(item.price * item.quantity).toFixed(2)} + +
+ ))} + +
+ + Subtotal + + ${subtotal.toFixed(2)} +
+
+ + Tax (10%) + + ${tax.toFixed(2)} +
+ +
+ Total + + ${total.toFixed(2)} + +
+
+
+
+ + + +
+
+ + + + 256-bit SSL Encryption + +
+

+ Your payment information is processed securely. We do not + store credit card details nor have access to your credit card + information. +

+
+
+
+
+
+
+ ); +} diff --git a/apps/ui/public/r/p-form-3.json b/apps/ui/public/r/p-form-3.json new file mode 100644 index 000000000..c9aed4085 --- /dev/null +++ b/apps/ui/public/r/p-form-3.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://ui.shadcn.com/schema/registry-item.json", + "name": "p-form-3", + "type": "registry:block", + "description": "Stripe checkout form with multiple payment methods", + "dependencies": [ + "lucide-react" + ], + "registryDependencies": [ + "@coss/badge", + "@coss/button", + "@coss/card", + "@coss/field", + "@coss/form", + "@coss/input", + "@coss/label", + "@coss/select", + "@coss/separator", + "@coss/spinner", + "@coss/tabs" + ], + "files": [ + { + "path": "registry/default/particles/p-form-3.tsx", + "content": "\"use client\";\n\nimport {\n BanknoteIcon,\n CreditCardIcon,\n LockIcon,\n ShieldCheckIcon,\n SmartphoneIcon,\n} from \"lucide-react\";\nimport { type FormEvent, useState } from \"react\";\n\nimport { Badge } from \"@/registry/default/ui/badge\";\nimport { Button } from \"@/registry/default/ui/button\";\nimport {\n Card,\n CardDescription,\n CardHeader,\n CardPanel,\n CardTitle,\n} from \"@/registry/default/ui/card\";\nimport { Field, FieldError, FieldLabel } from \"@/registry/default/ui/field\";\nimport { Form } from \"@/registry/default/ui/form\";\nimport { Input } from \"@/registry/default/ui/input\";\nimport { Label } from \"@/registry/default/ui/label\";\nimport {\n Select,\n SelectItem,\n SelectPopup,\n SelectTrigger,\n SelectValue,\n} from \"@/registry/default/ui/select\";\nimport { Separator } from \"@/registry/default/ui/separator\";\nimport { Spinner } from \"@/registry/default/ui/spinner\";\nimport { Tabs, TabsList, TabsPanel, TabsTab } from \"@/registry/default/ui/tabs\";\n\nconst countryOptions = [\n { label: \"United States\", value: \"us\" },\n { label: \"Canada\", value: \"ca\" },\n { label: \"United Kingdom\", value: \"uk\" },\n { label: \"Germany\", value: \"de\" },\n { label: \"France\", value: \"fr\" },\n { label: \"Australia\", value: \"au\" },\n];\n\nconst sepaCountryOptions = [\n { label: \"Germany\", value: \"de\" },\n { label: \"France\", value: \"fr\" },\n { label: \"Netherlands\", value: \"nl\" },\n { label: \"Belgium\", value: \"be\" },\n { label: \"Austria\", value: \"at\" },\n { label: \"Spain\", value: \"es\" },\n { label: \"Italy\", value: \"it\" },\n];\n\nconst orderItems = [\n { id: \"pro-plan\", name: \"Pro Plan (Annual)\", price: 199.0, quantity: 1 },\n { id: \"seats\", name: \"Additional Seats (5)\", price: 49.0, quantity: 1 },\n];\n\nfunction ApplePayIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nfunction GooglePayIcon({ className }: { className?: string }) {\n return (\n \n \n \n );\n}\n\nexport default function Particle() {\n const [loading, setLoading] = useState(false);\n const [cardNumber, setCardNumber] = useState(\"\");\n const [expiry, setExpiry] = useState(\"\");\n const [cvc, setCvc] = useState(\"\");\n const [iban, setIban] = useState(\"\");\n\n const subtotal = orderItems.reduce(\n (sum, item) => sum + item.price * item.quantity,\n 0,\n );\n const tax = subtotal * 0.1;\n const total = subtotal + tax;\n\n const formatCardNumber = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").replace(/[^0-9]/gi, \"\");\n const matches = v.match(/\\d{4,16}/g);\n const match = matches?.[0] || \"\";\n const parts = [];\n for (let i = 0, len = match.length; i < len; i += 4) {\n parts.push(match.substring(i, i + 4));\n }\n if (parts.length) {\n return parts.join(\" \");\n }\n return value;\n };\n\n const formatExpiry = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").replace(/[^0-9]/gi, \"\");\n if (v.length >= 2) {\n return `${v.substring(0, 2)}/${v.substring(2, 4)}`;\n }\n return v;\n };\n\n const formatIban = (value: string) => {\n const v = value.replace(/\\s+/g, \"\").toUpperCase();\n const parts = [];\n for (let i = 0, len = v.length; i < len; i += 4) {\n parts.push(v.substring(i, i + 4));\n }\n return parts.join(\" \");\n };\n\n const onSubmit = async (e: FormEvent) => {\n e.preventDefault();\n setLoading(true);\n await new Promise((r) => setTimeout(r, 2000));\n setLoading(false);\n alert(\"Payment successful! Thank you for your purchase.\");\n };\n\n const handleWalletPayment = async (wallet: string) => {\n setLoading(true);\n await new Promise((r) => setTimeout(r, 2000));\n setLoading(false);\n alert(`${wallet} payment successful! Thank you for your purchase.`);\n };\n\n return (\n
\n \n \n
\n \n Payment Details\n
\n \n Choose your preferred payment method\n \n
\n \n \n \n \n \n Card\n \n \n \n Wallet\n \n \n \n SEPA\n \n \n\n \n
\n
\n
\n \n
\n \n First Name\n \n Please enter your first name.\n \n \n Last Name\n \n Please enter your last name.\n \n
\n \n Email\n \n Please enter a valid email.\n \n
\n\n \n\n
\n \n \n Card Number\n \n setCardNumber(formatCardNumber(e.target.value))\n }\n placeholder=\"4242 4242 4242 4242\"\n required\n type=\"text\"\n value={cardNumber}\n />\n Please enter a valid card number.\n \n
\n \n Expiration Date\n \n setExpiry(formatExpiry(e.target.value))\n }\n placeholder=\"MM/YY\"\n required\n type=\"text\"\n value={expiry}\n />\n \n Please enter a valid expiry date.\n \n \n \n CVC\n \n setCvc(e.target.value.replace(/[^0-9]/g, \"\"))\n }\n placeholder=\"123\"\n required\n type=\"text\"\n value={cvc}\n />\n Please enter a valid CVC.\n \n
\n
\n\n \n\n
\n \n \n Country\n \n \n \n Street Address\n \n Please enter your address.\n \n
\n \n City\n \n Please enter your city.\n \n \n State\n \n Please enter your state.\n \n \n ZIP Code\n \n Please enter your ZIP code.\n \n
\n
\n
\n
\n \n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay ${total.toFixed(2)}\n \n )}\n \n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n
\n\n \n
\n
\n \n

\n Pay quickly and securely with your preferred digital wallet.\n

\n
\n\n
\n handleWalletPayment(\"Apple Pay\")}\n size=\"lg\"\n type=\"button\"\n >\n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay with Apple Pay\n \n )}\n \n\n handleWalletPayment(\"Google Pay\")}\n size=\"lg\"\n type=\"button\"\n variant=\"outline\"\n >\n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay with Google Pay\n \n )}\n \n
\n\n \n\n
\n
\n \n

Digital Wallet Payment

\n

\n When you click a wallet button, you'll be prompted to\n authenticate with your device (Face ID, Touch ID, or\n fingerprint).\n

\n
\n
\n\n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n\n \n
\n
\n
\n \n

\n Pay directly from your European bank account. Available\n for customers in the Single Euro Payments Area.\n

\n
\n\n
\n \n \n Full Name\n \n \n Please enter the account holder name.\n \n \n \n Email\n \n Please enter a valid email.\n \n
\n\n \n\n
\n \n \n IBAN\n setIban(formatIban(e.target.value))}\n placeholder=\"DE89 3704 0044 0532 0130 00\"\n required\n type=\"text\"\n value={iban}\n />\n Please enter a valid IBAN.\n \n \n Country\n \n \n
\n\n
\n

\n By providing your IBAN and confirming this payment, you\n authorize (A) this company and Stripe, our payment service\n provider, to send instructions to your bank to debit your\n account and (B) your bank to debit your account in\n accordance with those instructions. You are entitled to a\n refund from your bank under the terms and conditions of\n your agreement with your bank.\n

\n
\n
\n
\n \n {loading ? (\n <>\n \n Processing...\n \n ) : (\n <>\n \n Pay ${total.toFixed(2)} via SEPA\n \n )}\n \n
\n \n \n Secured by Stripe. Your payment info is encrypted.\n \n
\n
\n
\n
\n
\n
\n
\n\n
\n \n \n Order Summary\n \n \n
\n {orderItems.map((item) => (\n
\n
\n {item.name}\n \n Qty: {item.quantity}\n \n
\n \n ${(item.price * item.quantity).toFixed(2)}\n \n
\n ))}\n \n
\n Subtotal\n ${subtotal.toFixed(2)}\n
\n
\n Tax (10%)\n ${tax.toFixed(2)}\n
\n \n
\n Total\n \n ${total.toFixed(2)}\n \n
\n
\n
\n
\n\n \n \n
\n
\n \n \n Secure\n \n \n 256-bit SSL Encryption\n \n
\n

\n Your payment information is processed securely. We do not store\n credit card details nor have access to your credit card\n information.\n

\n
\n
\n
\n
\n
\n );\n}\n", + "type": "registry:block" + } + ], + "meta": { + "className": "**:data-[slot=preview]:w-full" + }, + "categories": [ + "card", + "form", + "input", + "select", + "tabs" + ] +} \ No newline at end of file