eny.space Landingpage
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

refactor(global): reset UI stuff

Sam Sauer 259f4253 6b3ba289

+86 -1129
-80
app/actions/stripe.ts
··· 1 - "use server"; 2 - 3 - import type { Stripe } from "stripe"; 4 - 5 - import { headers } from "next/headers"; 6 - 7 - import { CURRENCY } from "@/config"; 8 - import { formatAmountForStripe } from "@/utils/stripe-helpers"; 9 - import { stripe } from "@/lib/stripe"; 10 - 11 - export async function createCheckoutSession( 12 - data: FormData 13 - ): Promise<{ client_secret: string | null; url: string | null }> { 14 - const ui_mode = data.get( 15 - "uiMode" 16 - ) as Stripe.Checkout.SessionCreateParams.UiMode; 17 - 18 - const headersList = await headers(); 19 - const originHeader = headersList.get("origin"); 20 - const hostHeader = headersList.get("host"); 21 - 22 - let origin: string; 23 - if (originHeader) { 24 - origin = originHeader; 25 - } else if (hostHeader) { 26 - origin = `https://${hostHeader}`; 27 - } else { 28 - origin = process.env.NEXT_PUBLIC_APP_URL || "http://localhost:3000"; 29 - } 30 - 31 - const checkoutSession: Stripe.Checkout.Session = 32 - await stripe.checkout.sessions.create({ 33 - mode: "payment", 34 - submit_type: "pay", 35 - line_items: [ 36 - { 37 - quantity: 1, 38 - price_data: { 39 - currency: CURRENCY, 40 - product_data: { 41 - name: "Hosting Service", 42 - }, 43 - unit_amount: formatAmountForStripe( 44 - Number(data.get("customDonation") as string), 45 - CURRENCY 46 - ), 47 - }, 48 - }, 49 - ], 50 - ...(ui_mode === "hosted" && { 51 - success_url: `${origin}/checkout/result?session_id={CHECKOUT_SESSION_ID}`, 52 - cancel_url: `${origin}/checkout`, 53 - }), 54 - ...(ui_mode === "embedded" && { 55 - return_url: `${origin}/checkout/result?session_id={CHECKOUT_SESSION_ID}`, 56 - }), 57 - ui_mode, 58 - }); 59 - 60 - return { 61 - client_secret: checkoutSession.client_secret, 62 - url: checkoutSession.url, 63 - }; 64 - } 65 - 66 - export async function createPaymentIntent( 67 - data: FormData 68 - ): Promise<{ client_secret: string }> { 69 - const paymentIntent: Stripe.PaymentIntent = 70 - await stripe.paymentIntents.create({ 71 - amount: formatAmountForStripe( 72 - Number(data.get("customDonation") as string), 73 - CURRENCY 74 - ), 75 - automatic_payment_methods: { enabled: true }, 76 - currency: CURRENCY, 77 - }); 78 - 79 - return { client_secret: paymentIntent.client_secret as string }; 80 - }
-17
app/checkout/page.tsx
··· 1 - import type { Metadata } from "next"; 2 - 3 - import CheckoutForm from "@/components/CheckoutForm"; 4 - 5 - export const metadata: Metadata = { 6 - title: "Purchase Hosting", 7 - }; 8 - 9 - export default function DonatePage(): JSX.Element { 10 - return ( 11 - <div className="page-container"> 12 - <h1>Purchase Hosting Service</h1> 13 - <p>Select your hosting plan and complete your purchase</p> 14 - <CheckoutForm uiMode="hosted" /> 15 - </div> 16 - ); 17 - }
-5
app/checkout/result/error.tsx
··· 1 - "use client"; 2 - 3 - export default function Error({ error }: { error: Error }) { 4 - return <h2>{error.message}</h2>; 5 - }
-18
app/checkout/result/layout.tsx
··· 1 - import type { Metadata } from "next"; 2 - 3 - export const metadata: Metadata = { 4 - title: "Checkout Session Result", 5 - }; 6 - 7 - export default function ResultLayout({ 8 - children, 9 - }: { 10 - children: React.ReactNode; 11 - }): JSX.Element { 12 - return ( 13 - <div className="page-container"> 14 - <h1>Checkout Session Result</h1> 15 - {children} 16 - </div> 17 - ); 18 - }
-82
app/checkout/result/page.tsx
··· 1 - import type { Stripe } from "stripe"; 2 - 3 - import PrintObject from "@/components/PrintObject"; 4 - import { stripe } from "@/lib/stripe"; 5 - 6 - import Link from "next/link"; 7 - 8 - export default async function ResultPage({ 9 - searchParams, 10 - }: { 11 - searchParams: Promise<{ session_id?: string | string[] }>; 12 - }): Promise<JSX.Element> { 13 - const params = await searchParams; 14 - const sessionId = Array.isArray(params.session_id) 15 - ? params.session_id[0] 16 - : params.session_id; 17 - 18 - if (!sessionId) { 19 - return ( 20 - <div className="page-container"> 21 - <h2>No session found</h2> 22 - <p> 23 - It looks like you didn't complete a checkout session, or the session 24 - information is missing. 25 - </p> 26 - <Link 27 - href="/checkout" 28 - style={{ 29 - display: "inline-block", 30 - padding: "12px 24px", 31 - borderRadius: "6px", 32 - marginTop: "16px", 33 - backgroundColor: "#8f6ed5", 34 - color: "#fff", 35 - textDecoration: "none", 36 - }} 37 - > 38 - Return to purchase page 39 - </Link> 40 - </div> 41 - ); 42 - } 43 - 44 - try { 45 - const checkoutSession: Stripe.Checkout.Session = 46 - await stripe.checkout.sessions.retrieve(sessionId, { 47 - expand: ["line_items", "payment_intent"], 48 - }); 49 - 50 - const paymentIntent = 51 - checkoutSession.payment_intent as Stripe.PaymentIntent; 52 - 53 - return ( 54 - <> 55 - <h2>Status: {paymentIntent.status}</h2> 56 - <h3>Checkout Session response:</h3> 57 - <PrintObject content={checkoutSession} /> 58 - </> 59 - ); 60 - } catch (error) { 61 - return ( 62 - <div className="page-container"> 63 - <h2>Error retrieving session</h2> 64 - <p>The checkout session could not be retrieved. Please try again.</p> 65 - <Link 66 - href="/checkout" 67 - style={{ 68 - display: "inline-block", 69 - padding: "12px 24px", 70 - borderRadius: "6px", 71 - marginTop: "16px", 72 - backgroundColor: "#8f6ed5", 73 - color: "#fff", 74 - textDecoration: "none", 75 - }} 76 - > 77 - Return to purchase page 78 - </Link> 79 - </div> 80 - ); 81 - } 82 - }
-83
app/components/CheckoutForm.tsx
··· 1 - "use client"; 2 - 3 - import type Stripe from "stripe"; 4 - 5 - import React, { useState } from "react"; 6 - 7 - import CustomDonationInput from "@/components/CustomDonationInput"; 8 - import TestCards from "@/components/TestCards"; 9 - 10 - import { formatAmountForDisplay } from "@/utils/stripe-helpers"; 11 - import * as config from "@/config"; 12 - import { createCheckoutSession } from "@/actions/stripe"; 13 - import getStripe from "@/utils/get-stripejs"; 14 - import { 15 - EmbeddedCheckout, 16 - EmbeddedCheckoutProvider, 17 - } from "@stripe/react-stripe-js"; 18 - 19 - interface CheckoutFormProps { 20 - uiMode: Stripe.Checkout.SessionCreateParams.UiMode; 21 - } 22 - 23 - export default function CheckoutForm(props: CheckoutFormProps): JSX.Element { 24 - const [loading] = useState<boolean>(false); 25 - const [input, setInput] = useState<{ customDonation: number }>({ 26 - customDonation: Math.round(config.MAX_AMOUNT / config.AMOUNT_STEP), 27 - }); 28 - const [clientSecret, setClientSecret] = useState<string | null>(null); 29 - 30 - const handleInputChange: React.ChangeEventHandler<HTMLInputElement> = ( 31 - e 32 - ): void => 33 - setInput({ 34 - ...input, 35 - [e.currentTarget.name]: e.currentTarget.value, 36 - }); 37 - 38 - const formAction = async (data: FormData): Promise<void> => { 39 - const uiMode = data.get( 40 - "uiMode" 41 - ) as Stripe.Checkout.SessionCreateParams.UiMode; 42 - const { client_secret, url } = await createCheckoutSession(data); 43 - 44 - if (uiMode === "embedded") return setClientSecret(client_secret); 45 - 46 - window.location.assign(url as string); 47 - }; 48 - 49 - return ( 50 - <> 51 - <form action={formAction}> 52 - <input type="hidden" name="uiMode" value={props.uiMode} /> 53 - <CustomDonationInput 54 - className="checkout-style" 55 - name="customDonation" 56 - min={config.MIN_AMOUNT} 57 - max={config.MAX_AMOUNT} 58 - step={config.AMOUNT_STEP} 59 - currency={config.CURRENCY} 60 - onChange={handleInputChange} 61 - value={input.customDonation} 62 - /> 63 - <TestCards /> 64 - <button 65 - className="checkout-style-background" 66 - type="submit" 67 - disabled={loading} 68 - > 69 - Purchase{" "} 70 - {formatAmountForDisplay(input.customDonation, config.CURRENCY)} 71 - </button> 72 - </form> 73 - {clientSecret ? ( 74 - <EmbeddedCheckoutProvider 75 - stripe={getStripe()} 76 - options={{ clientSecret }} 77 - > 78 - <EmbeddedCheckout /> 79 - </EmbeddedCheckoutProvider> 80 - ) : null} 81 - </> 82 - ); 83 - }
-38
app/components/CustomDonationInput.tsx
··· 1 - import { formatAmountForDisplay } from "@/utils/stripe-helpers"; 2 - 3 - export default function CustomDonationInput({ 4 - name, 5 - min, 6 - max, 7 - currency, 8 - step, 9 - onChange, 10 - value, 11 - className, 12 - }: { 13 - name: string; 14 - min: number; 15 - max: number; 16 - currency: string; 17 - step: number; 18 - onChange: (e: React.ChangeEvent<HTMLInputElement>) => void; 19 - value: number; 20 - className?: string; 21 - }): JSX.Element { 22 - return ( 23 - <label> 24 - Service amount ({formatAmountForDisplay(min, currency)}- 25 - {formatAmountForDisplay(max, currency)}): 26 - <input 27 - type="range" 28 - name={name} 29 - min={min} 30 - max={max} 31 - step={step} 32 - onChange={onChange} 33 - value={value} 34 - className={className} 35 - ></input> 36 - </label> 37 - ); 38 - }
-10
app/components/PrintObject.tsx
··· 1 - import type { Stripe } from "stripe"; 2 - 3 - export default function PrintObject({ 4 - content, 5 - }: { 6 - content: Stripe.PaymentIntent | Stripe.Checkout.Session; 7 - }): JSX.Element { 8 - const formattedContent: string = JSON.stringify(content, null, 2); 9 - return <pre>{formattedContent}</pre>; 10 - }
-11
app/components/TestCards.tsx
··· 1 - export default function TestCards(): JSX.Element { 2 - return ( 3 - <div className="test-card-notice"> 4 - Use test cards for testing, e.g.{" "} 5 - <div className="card-number"> 6 - 4242<span></span>4242<span></span>4242<span></span>4242 7 - </div> 8 - . 9 - </div> 10 - ); 11 - }
+55 -193
app/dashboard/dashboard-client.tsx
··· 65 65 } 66 66 }; 67 67 68 - // Show subscription management 69 68 const hasSubscription = !!subscription; 70 69 const isCanceled = 71 70 subscription?.status === "canceled" || subscription?.status === "past_due"; 72 71 73 72 if (!hasSubscription) { 74 73 return ( 75 - <div 76 - style={{ 77 - marginTop: "32px", 78 - padding: "24px", 79 - border: "1px solid #ccc", 80 - borderRadius: "8px", 81 - }} 82 - > 74 + <div> 83 75 <h2>Subscribe to Access</h2> 84 76 <p>You need an active subscription to access the server features.</p> 85 - <button 86 - onClick={handleSubscribe} 87 - disabled={loading} 88 - style={{ 89 - marginTop: "16px", 90 - padding: "12px 24px", 91 - borderRadius: "6px", 92 - backgroundColor: "#000000", 93 - color: "#ffffff", 94 - border: "1px solid #ffffff", 95 - cursor: loading ? "not-allowed" : "pointer", 96 - fontWeight: 600, 97 - opacity: loading ? 0.6 : 1, 98 - }} 99 - > 77 + <button onClick={handleSubscribe} disabled={loading}> 100 78 {loading ? "Loading..." : "Subscribe Now"} 101 79 </button> 102 80 </div> ··· 105 83 106 84 if (isCanceled) { 107 85 return ( 108 - <div 109 - style={{ 110 - marginTop: "32px", 111 - padding: "24px", 112 - border: "1px solid #ccc", 113 - borderRadius: "8px", 114 - backgroundColor: "#f5f5f5", 115 - }} 116 - > 117 - <h2 style={{ color: "#666", marginTop: 0 }}>Subscription Canceled</h2> 86 + <div> 87 + <h2>Subscription Canceled</h2> 118 88 <p> 119 89 Your subscription has been canceled. Subscribe again to regain access. 120 90 </p> 121 - <button 122 - onClick={handleSubscribe} 123 - disabled={loading} 124 - style={{ 125 - marginTop: "16px", 126 - padding: "12px 24px", 127 - borderRadius: "6px", 128 - backgroundColor: "#000000", 129 - color: "#ffffff", 130 - border: "1px solid #ffffff", 131 - cursor: loading ? "not-allowed" : "pointer", 132 - fontWeight: 600, 133 - opacity: loading ? 0.6 : 1, 134 - }} 135 - > 91 + <button onClick={handleSubscribe} disabled={loading}> 136 92 {loading ? "Loading..." : "Subscribe Again"} 137 93 </button> 138 94 </div> ··· 203 159 }; 204 160 205 161 const isCanceling = subscription?.cancel_at_period_end === true; 206 - const statusColor = isCanceling ? "#ff9800" : "#4caf50"; 207 - const statusBg = isCanceling ? "#fff3e0" : "#f0f9f0"; 208 162 209 163 return ( 210 - <div style={{ marginTop: "32px" }}> 211 - <div 212 - style={{ 213 - padding: "24px", 214 - border: `1px solid ${statusColor}`, 215 - borderRadius: "8px", 216 - backgroundColor: statusBg, 217 - marginBottom: "24px", 218 - }} 219 - > 220 - <h2 style={{ color: statusColor, marginTop: 0 }}> 221 - {isCanceling ? "⚠️ Subscription Canceling" : "✓ Active Subscription"} 222 - </h2> 223 - {subscription && ( 224 - <div style={{ marginBottom: "16px" }}> 164 + <div> 165 + <h2>{isCanceling ? "Subscription Canceling" : "Active Subscription"}</h2> 166 + {subscription && ( 167 + <div> 168 + <p> 169 + <strong>Status:</strong> {subscription.status} 170 + </p> 171 + {subscription.current_period_end && ( 225 172 <p> 226 - <strong>Status:</strong> {subscription.status} 173 + <strong>{isCanceling ? "Access until:" : "Renews:"}</strong>{" "} 174 + {new Date(subscription.current_period_end).toLocaleDateString()} 227 175 </p> 228 - {subscription.current_period_end && ( 229 - <p> 230 - <strong>{isCanceling ? "Access until:" : "Renews:"}</strong>{" "} 231 - {new Date(subscription.current_period_end).toLocaleDateString()} 232 - </p> 233 - )} 234 - {isCanceling && ( 235 - <p style={{ color: "#ff9800", fontWeight: 600 }}> 236 - Your subscription will cancel at the end of the billing period. 237 - </p> 238 - )} 239 - </div> 240 - )} 241 - 242 - <div 243 - style={{ 244 - display: "flex", 245 - flexDirection: "column", 246 - gap: "8px", 247 - marginTop: "16px", 248 - }} 249 - > 250 - <button 251 - onClick={handleManageBilling} 252 - disabled={actionLoading !== null} 253 - style={{ 254 - padding: "10px 20px", 255 - borderRadius: "6px", 256 - backgroundColor: "#000000", 257 - color: "#ffffff", 258 - border: "1px solid #ffffff", 259 - cursor: actionLoading !== null ? "not-allowed" : "pointer", 260 - fontWeight: 600, 261 - opacity: actionLoading !== null ? 0.6 : 1, 262 - }} 263 - > 264 - {actionLoading === "billing" 265 - ? "Loading..." 266 - : "Manage Payment Method"} 267 - </button> 268 - 269 - {isCanceling ? ( 270 - <button 271 - onClick={handleResume} 272 - disabled={actionLoading !== null} 273 - style={{ 274 - padding: "10px 20px", 275 - borderRadius: "6px", 276 - backgroundColor: "#4caf50", 277 - color: "#ffffff", 278 - border: "1px solid #4caf50", 279 - cursor: actionLoading !== null ? "not-allowed" : "pointer", 280 - fontWeight: 600, 281 - opacity: actionLoading !== null ? 0.6 : 1, 282 - }} 283 - > 284 - {actionLoading === "resume" 285 - ? "Loading..." 286 - : "Resume Subscription"} 287 - </button> 288 - ) : ( 289 - <button 290 - onClick={handleCancel} 291 - disabled={actionLoading !== null} 292 - style={{ 293 - padding: "10px 20px", 294 - borderRadius: "6px", 295 - backgroundColor: "transparent", 296 - color: "#ff5722", 297 - border: "1px solid #ff5722", 298 - cursor: actionLoading !== null ? "not-allowed" : "pointer", 299 - fontWeight: 600, 300 - opacity: actionLoading !== null ? 0.6 : 1, 301 - }} 302 - > 303 - {actionLoading === "cancel" 304 - ? "Loading..." 305 - : "Cancel Subscription"} 306 - </button> 176 + )} 177 + {isCanceling && ( 178 + <p> 179 + Your subscription will cancel at the end of the billing period. 180 + </p> 307 181 )} 308 182 </div> 309 - </div> 183 + )} 310 184 311 - <div 312 - style={{ 313 - padding: "24px", 314 - border: "1px solid #ccc", 315 - borderRadius: "8px", 316 - }} 317 - > 318 - <h2>Server Actions</h2> 319 - <p>You have access to the following server endpoints:</p> 185 + <p> 186 + <button onClick={handleManageBilling} disabled={actionLoading !== null}> 187 + {actionLoading === "billing" 188 + ? "Loading..." 189 + : "Manage Payment Method"} 190 + </button> 191 + </p> 320 192 321 - <div 322 - style={{ 323 - display: "flex", 324 - flexDirection: "column", 325 - gap: "12px", 326 - marginTop: "16px", 327 - }} 328 - > 329 - <button 330 - onClick={() => handleServerCall("action1")} 331 - style={{ 332 - padding: "12px 24px", 333 - borderRadius: "6px", 334 - backgroundColor: "#000000", 335 - color: "#ffffff", 336 - border: "1px solid #ffffff", 337 - cursor: "pointer", 338 - fontWeight: 600, 339 - }} 340 - > 341 - Call Server Action 1 193 + <p> 194 + {isCanceling ? ( 195 + <button onClick={handleResume} disabled={actionLoading !== null}> 196 + {actionLoading === "resume" 197 + ? "Loading..." 198 + : "Resume Subscription"} 342 199 </button> 343 - 344 - <button 345 - onClick={() => handleServerCall("action2")} 346 - style={{ 347 - padding: "12px 24px", 348 - borderRadius: "6px", 349 - backgroundColor: "#000000", 350 - color: "#ffffff", 351 - border: "1px solid #ffffff", 352 - cursor: "pointer", 353 - fontWeight: 600, 354 - }} 355 - > 356 - Call Server Action 2 200 + ) : ( 201 + <button onClick={handleCancel} disabled={actionLoading !== null}> 202 + {actionLoading === "cancel" 203 + ? "Loading..." 204 + : "Cancel Subscription"} 357 205 </button> 358 - </div> 359 - </div> 206 + )} 207 + </p> 208 + 209 + <hr /> 210 + <h2>Server Actions</h2> 211 + <p>You have access to the following server endpoints:</p> 212 + <p> 213 + <button onClick={() => handleServerCall("action1")}> 214 + Call Server Action 1 215 + </button> 216 + </p> 217 + <p> 218 + <button onClick={() => handleServerCall("action2")}> 219 + Call Server Action 2 220 + </button> 221 + </p> 360 222 </div> 361 223 ); 362 224 }
+1 -3
app/dashboard/page.tsx
··· 13 13 redirect("/login"); 14 14 } 15 15 16 - // Always fetch subscription status directly from Stripe (source of truth) 17 16 const { subscribed, subscription } = await getSubscriptionStatus(); 18 17 19 18 return ( 20 - <main className="page-container"> 19 + <main> 21 20 <h1>Dashboard</h1> 22 21 <p>Welcome, {user.email}!</p> 23 - 24 22 <DashboardClient 25 23 subscribed={subscribed} 26 24 subscription={subscription}
-432
app/globals.css
··· 1 1 @import "tailwindcss"; 2 - @import "tw-animate-css"; 3 - 4 - @custom-variant dark (&:is(.dark *)); 5 - 6 - /* Variables */ 7 - :root { 8 - --body-color: #050505; 9 - --body-text-color: #f5f5f5; 10 - --checkout-color: #8f6ed5; 11 - --elements-color: #7c7cff; 12 - --h1-color: #f9fafb; 13 - --h2-color: #e5e7eb; 14 - --h3-color: #9ca3af; 15 - --radius: 0.625rem; 16 - --container-width-max: 1280px; 17 - --page-width-max: 600px; 18 - --transition-duration: 2s; 19 - --background: oklch(1 0 0); 20 - --foreground: oklch(0.141 0.005 285.823); 21 - --card: oklch(1 0 0); 22 - --card-foreground: oklch(0.141 0.005 285.823); 23 - --popover: oklch(1 0 0); 24 - --popover-foreground: oklch(0.141 0.005 285.823); 25 - --primary: oklch(0.21 0.006 285.885); 26 - --primary-foreground: oklch(0.985 0 0); 27 - --secondary: oklch(0.967 0.001 286.375); 28 - --secondary-foreground: oklch(0.21 0.006 285.885); 29 - --muted: oklch(0.967 0.001 286.375); 30 - --muted-foreground: oklch(0.552 0.016 285.938); 31 - --accent: oklch(0.967 0.001 286.375); 32 - --accent-foreground: oklch(0.21 0.006 285.885); 33 - --destructive: oklch(0.577 0.245 27.325); 34 - --border: oklch(0.92 0.004 286.32); 35 - --input: oklch(0.92 0.004 286.32); 36 - --ring: oklch(0.705 0.015 286.067); 37 - --chart-1: oklch(0.646 0.222 41.116); 38 - --chart-2: oklch(0.6 0.118 184.704); 39 - --chart-3: oklch(0.398 0.07 227.392); 40 - --chart-4: oklch(0.828 0.189 84.429); 41 - --chart-5: oklch(0.769 0.188 70.08); 42 - --sidebar: oklch(0.985 0 0); 43 - --sidebar-foreground: oklch(0.141 0.005 285.823); 44 - --sidebar-primary: oklch(0.21 0.006 285.885); 45 - --sidebar-primary-foreground: oklch(0.985 0 0); 46 - --sidebar-accent: oklch(0.967 0.001 286.375); 47 - --sidebar-accent-foreground: oklch(0.21 0.006 285.885); 48 - --sidebar-border: oklch(0.92 0.004 286.32); 49 - --sidebar-ring: oklch(0.705 0.015 286.067); 50 - } 51 - 52 - body { 53 - margin: 0; 54 - padding: 0; 55 - background: var(--body-color); 56 - color: var(--body-text-color); 57 - overflow-y: scroll; 58 - } 59 - 60 - * { 61 - box-sizing: border-box; 62 - -webkit-tap-highlight-color: rgba(255, 255, 255, 0); 63 - } 64 - 65 - #__next { 66 - display: flex; 67 - justify-content: center; 68 - } 69 - 70 - .container { 71 - max-width: var(--container-width-max); 72 - padding: 45px 25px; 73 - display: flex; 74 - flex-direction: row; 75 - } 76 - 77 - .page-container { 78 - padding-bottom: 60px; 79 - max-width: var(--page-width-max); 80 - } 81 - 82 - h1 { 83 - font-weight: 600; 84 - color: var(--h1-color); 85 - margin: 6px 0 12px; 86 - font-size: 27px; 87 - line-height: 32px; 88 - } 89 - 90 - h1 span.light { 91 - color: var(--h3-color); 92 - } 93 - 94 - h2 { 95 - color: var(--h2-color); 96 - margin: 8px 0; 97 - } 98 - 99 - h3 { 100 - font-size: 17px; 101 - color: var(--h3-color); 102 - margin: 8px 0; 103 - } 104 - 105 - a { 106 - color: #ffffff; 107 - text-decoration: none; 108 - } 109 - 110 - header { 111 - position: relative; 112 - flex: 0 0 250px; 113 - padding-right: 48px; 114 - } 115 - 116 - .header-content { 117 - position: sticky; 118 - top: 45px; 119 - } 120 - 121 - .logo img { 122 - height: 20px; 123 - margin-bottom: 52px; 124 - } 125 - 126 - ul, 127 - li { 128 - list-style: none; 129 - padding: 0; 130 - margin: 0; 131 - } 132 - 133 - .card-list { 134 - display: flex; 135 - flex-wrap: wrap; 136 - align-content: flex-start; 137 - padding-top: 64px; 138 - } 139 - 140 - .card { 141 - display: block; 142 - border-radius: 10px; 143 - position: relative; 144 - padding: 12px; 145 - height: 320px; 146 - flex: 0 0 33%; 147 - min-width: 304px; 148 - width: 33%; 149 - margin: 0 20px 20px 0; 150 - text-decoration: none; 151 - box-shadow: 0 20px 40px rgba(0, 0, 0, 0.6), 152 - 0 0 0 1px rgba(255, 255, 255, 0.08); 153 - background: #0b0b0f; 154 - } 155 - .card h2 { 156 - color: #fff; 157 - } 158 - .card h2.bottom { 159 - position: absolute; 160 - bottom: 10px; 161 - } 162 - 163 - .card img { 164 - width: 80%; 165 - position: absolute; 166 - top: 50%; 167 - left: 50%; 168 - transform: translate(-50%, -50%); 169 - } 170 - 171 - .error-message { 172 - color: #ef2961; 173 - } 174 - 175 - .FormRow, 176 - fieldset, 177 - input[type="number"], 178 - input[type="text"] { 179 - border-radius: var(--radius); 180 - padding: 5px 12px; 181 - width: 100%; 182 - background: #111827; 183 - color: var(--body-text-color); 184 - appearance: none; 185 - font-size: 16px; 186 - margin-top: 10px; 187 - } 188 - 189 - input[type="range"] { 190 - margin: 5px 0; 191 - width: 100%; 192 - } 193 - 194 - button { 195 - border-radius: var(--radius); 196 - color: white; 197 - font-size: larger; 198 - border: 0; 199 - padding: 12px 16px; 200 - margin-top: 10px; 201 - font-weight: 600; 202 - cursor: pointer; 203 - transition: all 0.2s ease; 204 - display: block; 205 - width: 100%; 206 - } 207 - button:disabled { 208 - opacity: 0.5; 209 - cursor: not-allowed; 210 - } 211 - 212 - .elements-style { 213 - color: var(--elements-color); 214 - border: 1px solid var(--elements-color); 215 - } 216 - .elements-style-background { 217 - background: var(--elements-color); 218 - transition: box-shadow var(--transition-duration); 219 - } 220 - .card.elements-style-background:hover { 221 - box-shadow: 20px 20px 60px #464e9c, -20px -20px 60px #8896ff; 222 - } 223 - .checkout-style { 224 - color: var(--checkout-color); 225 - border: 1px solid var(--checkout-color); 226 - } 227 - .checkout-style-background { 228 - background: #000000; 229 - color: #ffffff; 230 - border: 1px solid #ffffff; 231 - transition: box-shadow var(--transition-duration), transform 0.15s ease; 232 - } 233 - .card.checkout-style-background:hover { 234 - box-shadow: 0 0 0 2px #ffffff; 235 - transform: translateY(-2px); 236 - } 237 - 238 - /* Test card number */ 239 - .test-card-notice { 240 - display: block; 241 - margin-block-start: 1em; 242 - margin-block-end: 1em; 243 - margin-inline-start: 0px; 244 - margin-inline-end: 0px; 245 - } 246 - .card-number { 247 - display: inline; 248 - white-space: nowrap; 249 - font-family: Menlo, Consolas, monospace; 250 - color: #3c4257; 251 - font-weight: 500; 252 - } 253 - .card-number span { 254 - display: inline-block; 255 - width: 4px; 256 - } 257 - 258 - /* Code block */ 259 - code, 260 - pre { 261 - font-family: "SF Mono", "IBM Plex Mono", "Menlo", monospace; 262 - font-size: 12px; 263 - background: rgba(255, 255, 255, 0.04); 264 - padding: 12px; 265 - border-radius: var(--radius); 266 - max-height: 500px; 267 - width: var(--page-width-max); 268 - overflow: auto; 269 - } 270 - 271 - .banner { 272 - max-width: 825px; 273 - margin: 0 auto; 274 - font-size: 14px; 275 - background: #111827; 276 - color: #e5e7eb; 277 - border-radius: 50px; 278 - box-shadow: 0 12px 30px rgba(0, 0, 0, 0.8), 279 - 0 0 0 1px rgba(255, 255, 255, 0.08); 280 - display: flex; 281 - align-items: center; 282 - box-sizing: border-box; 283 - padding: 15px; 284 - line-height: 1.15; 285 - position: fixed; 286 - bottom: 2vh; 287 - left: 0; 288 - right: 0; 289 - text-align: center; 290 - justify-content: center; 291 - } 292 - 293 - @media only screen and (max-width: 980px) { 294 - .container { 295 - flex-direction: column; 296 - } 297 - 298 - .header-content { 299 - max-width: 280px; 300 - position: relative; 301 - top: 0; 302 - } 303 - 304 - .card { 305 - margin: 0 20px 20px 0; 306 - box-shadow: none; 307 - } 308 - 309 - .card-list { 310 - padding-top: 0; 311 - } 312 - 313 - .banner { 314 - box-shadow: none; 315 - bottom: 0; 316 - } 317 - } 318 - 319 - @media only screen and (max-width: 600px) { 320 - .container { 321 - flex-direction: column; 322 - } 323 - 324 - .card { 325 - display: block; 326 - border-radius: 8px; 327 - flex: 1 0 100%; 328 - max-width: 100%; 329 - padding-left: 0; 330 - padding-right: 0; 331 - margin: 0 0 20px 0; 332 - box-shadow: none; 333 - } 334 - 335 - .card-list { 336 - padding-top: 0; 337 - } 338 - 339 - code, 340 - pre, 341 - h3 { 342 - display: none; 343 - } 344 - 345 - .banner { 346 - box-shadow: none; 347 - bottom: 0; 348 - } 349 - } 350 - 351 - @theme inline { 352 - --radius-sm: calc(var(--radius) - 4px); 353 - --radius-md: calc(var(--radius) - 2px); 354 - --radius-lg: var(--radius); 355 - --radius-xl: calc(var(--radius) + 4px); 356 - --radius-2xl: calc(var(--radius) + 8px); 357 - --radius-3xl: calc(var(--radius) + 12px); 358 - --radius-4xl: calc(var(--radius) + 16px); 359 - --color-background: var(--background); 360 - --color-foreground: var(--foreground); 361 - --color-card: var(--card); 362 - --color-card-foreground: var(--card-foreground); 363 - --color-popover: var(--popover); 364 - --color-popover-foreground: var(--popover-foreground); 365 - --color-primary: var(--primary); 366 - --color-primary-foreground: var(--primary-foreground); 367 - --color-secondary: var(--secondary); 368 - --color-secondary-foreground: var(--secondary-foreground); 369 - --color-muted: var(--muted); 370 - --color-muted-foreground: var(--muted-foreground); 371 - --color-accent: var(--accent); 372 - --color-accent-foreground: var(--accent-foreground); 373 - --color-destructive: var(--destructive); 374 - --color-border: var(--border); 375 - --color-input: var(--input); 376 - --color-ring: var(--ring); 377 - --color-chart-1: var(--chart-1); 378 - --color-chart-2: var(--chart-2); 379 - --color-chart-3: var(--chart-3); 380 - --color-chart-4: var(--chart-4); 381 - --color-chart-5: var(--chart-5); 382 - --color-sidebar: var(--sidebar); 383 - --color-sidebar-foreground: var(--sidebar-foreground); 384 - --color-sidebar-primary: var(--sidebar-primary); 385 - --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); 386 - --color-sidebar-accent: var(--sidebar-accent); 387 - --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); 388 - --color-sidebar-border: var(--sidebar-border); 389 - --color-sidebar-ring: var(--sidebar-ring); 390 - } 391 - 392 - .dark { 393 - --background: oklch(0.141 0.005 285.823); 394 - --foreground: oklch(0.985 0 0); 395 - --card: oklch(0.21 0.006 285.885); 396 - --card-foreground: oklch(0.985 0 0); 397 - --popover: oklch(0.21 0.006 285.885); 398 - --popover-foreground: oklch(0.985 0 0); 399 - --primary: oklch(0.92 0.004 286.32); 400 - --primary-foreground: oklch(0.21 0.006 285.885); 401 - --secondary: oklch(0.274 0.006 286.033); 402 - --secondary-foreground: oklch(0.985 0 0); 403 - --muted: oklch(0.274 0.006 286.033); 404 - --muted-foreground: oklch(0.705 0.015 286.067); 405 - --accent: oklch(0.274 0.006 286.033); 406 - --accent-foreground: oklch(0.985 0 0); 407 - --destructive: oklch(0.704 0.191 22.216); 408 - --border: oklch(1 0 0 / 10%); 409 - --input: oklch(1 0 0 / 15%); 410 - --ring: oklch(0.552 0.016 285.938); 411 - --chart-1: oklch(0.488 0.243 264.376); 412 - --chart-2: oklch(0.696 0.17 162.48); 413 - --chart-3: oklch(0.769 0.188 70.08); 414 - --chart-4: oklch(0.627 0.265 303.9); 415 - --chart-5: oklch(0.645 0.246 16.439); 416 - --sidebar: oklch(0.21 0.006 285.885); 417 - --sidebar-foreground: oklch(0.985 0 0); 418 - --sidebar-primary: oklch(0.488 0.243 264.376); 419 - --sidebar-primary-foreground: oklch(0.985 0 0); 420 - --sidebar-accent: oklch(0.274 0.006 286.033); 421 - --sidebar-accent-foreground: oklch(0.985 0 0); 422 - --sidebar-border: oklch(1 0 0 / 10%); 423 - --sidebar-ring: oklch(0.552 0.016 285.938); 424 - } 425 - 426 - @layer base { 427 - * { 428 - @apply border-border outline-ring/50; 429 - } 430 - body { 431 - @apply bg-background text-foreground; 432 - } 433 - }
+24 -64
app/layout.tsx
··· 1 1 import type { Metadata } from "next"; 2 2 import { SpeedInsights } from "@vercel/speed-insights/next"; 3 3 import { Analytics } from "@vercel/analytics/next"; 4 - import { Space_Grotesk } from "next/font/google"; 5 4 import Link from "next/link"; 6 5 import { createClient } from "@/lib/supabase/server"; 7 6 import { signOut } from "@/actions/auth"; 8 7 9 8 import "./globals.css"; 10 - 11 - const fontSans = Space_Grotesk({ 12 - subsets: ["latin"], 13 - weight: ["400", "500", "600", "700"], 14 - }); 15 9 16 10 interface LayoutProps { 17 11 children: React.ReactNode; ··· 36 30 37 31 return ( 38 32 <html lang="en"> 39 - <body className={fontSans.className}> 40 - <div className="container"> 41 - <header> 42 - <div className="header-content"> 43 - <Link 44 - href="/" 45 - style={{ textDecoration: "none", color: "inherit" }} 46 - > 47 - <h1>eny.space</h1> 48 - </Link> 49 - <nav 50 - style={{ 51 - display: "flex", 52 - flexDirection: "column", 53 - gap: "12px", 54 - marginTop: "24px", 55 - }} 56 - > 57 - {user ? ( 58 - <> 59 - <Link 60 - href="/dashboard" 61 - style={{ color: "var(--h2-color)" }} 62 - > 63 - Dashboard 64 - </Link> 65 - <form action={signOut} style={{ margin: 0 }}> 66 - <button 67 - type="submit" 68 - style={{ 69 - background: "none", 70 - border: "none", 71 - cursor: "pointer", 72 - color: "var(--h2-color)", 73 - padding: 0, 74 - textAlign: "left", 75 - font: "inherit", 76 - }} 77 - > 78 - Sign Out 79 - </button> 80 - </form> 81 - </> 82 - ) : ( 83 - <> 84 - <Link href="/login" style={{ color: "var(--h2-color)" }}> 85 - Login 86 - </Link> 87 - <Link href="/signup" style={{ color: "var(--h2-color)" }}> 88 - Sign Up 89 - </Link> 90 - </> 91 - )} 92 - </nav> 93 - </div> 94 - </header> 95 - {children} 96 - </div> 33 + <body> 34 + <header> 35 + <Link href="/">eny.space</Link> 36 + {" | "} 37 + <nav style={{ display: "inline" }}> 38 + {user ? ( 39 + <> 40 + <Link href="/dashboard">Dashboard</Link> 41 + {" | "} 42 + <form action={signOut} style={{ display: "inline" }}> 43 + <button type="submit">Sign Out</button> 44 + </form> 45 + </> 46 + ) : ( 47 + <> 48 + <Link href="/login">Login</Link> 49 + {" | "} 50 + <Link href="/signup">Sign Up</Link> 51 + </> 52 + )} 53 + </nav> 54 + </header> 55 + <hr /> 56 + {children} 97 57 <SpeedInsights /> 98 58 <Analytics /> 99 59 </body>
+4 -33
app/page.tsx
··· 8 8 9 9 export default function IndexPage(): JSX.Element { 10 10 return ( 11 - <main className="page-container"> 11 + <main> 12 12 <h1>Your own custom PDS in seconds</h1> 13 13 <h2>One-click ATProto hosting with eny.space</h2> 14 14 <p> ··· 31 31 you. 32 32 </p> 33 33 34 - <div 35 - style={{ display: "flex", gap: 12, marginTop: 24, flexWrap: "wrap" }} 36 - > 37 - <Link 38 - href="mailto:hello@krekeny.com?subject=I%27d%20like%20early%20access%20to%20eny.space%20PDS" 39 - style={{ 40 - display: "inline-block", 41 - padding: "12px 24px", 42 - borderRadius: 6, 43 - backgroundColor: "#000000", 44 - color: "#ffffff", 45 - border: "1px solid #ffffff", 46 - textDecoration: "none", 47 - fontWeight: 600, 48 - }} 49 - > 34 + <p> 35 + <Link href="mailto:hello@krekeny.com?subject=I%27d%20like%20early%20access%20to%20eny.space%20PDS"> 50 36 Request early access 51 37 </Link> 52 - <Link 53 - href="/checkout" 54 - style={{ 55 - display: "inline-block", 56 - padding: "12px 24px", 57 - borderRadius: 6, 58 - backgroundColor: "#ffffff", 59 - color: "#000000", 60 - border: "1px solid #ffffff", 61 - textDecoration: "none", 62 - fontWeight: 600, 63 - }} 64 - > 65 - Get yours 66 - </Link> 67 - </div> 38 + </p> 68 39 </main> 69 40 ); 70 41 }
-6
config/index.ts
··· 1 - export const CURRENCY = "eur"; 2 - // Set your amount limits: Use float for decimal currencies and 3 - // Integer for zero-decimal currencies: https://stripe.com/docs/currencies#zero-decimal. 4 - export const MIN_AMOUNT = 10.0; 5 - export const MAX_AMOUNT = 100.0; 6 - export const AMOUNT_STEP = 1.0;
+2 -2
package-lock.json
··· 18 18 "lucide-react": "^0.562.0", 19 19 "next": "latest", 20 20 "postcss": "^8.5.6", 21 - "react": "18.2.0", 22 - "react-dom": "18.2.0", 21 + "react": "^18.2.0", 22 + "react-dom": "^18.2.0", 23 23 "server-only": "0.0.1", 24 24 "stripe": "14.8.0", 25 25 "tailwind-merge": "^3.4.0",
-2
package.json
··· 7 7 }, 8 8 "dependencies": { 9 9 "@radix-ui/react-slot": "^1.2.4", 10 - "@stripe/react-stripe-js": "2.4.0", 11 - "@stripe/stripe-js": "2.2.2", 12 10 "@supabase/ssr": "^0.8.0", 13 11 "@supabase/supabase-js": "^2.90.1", 14 12 "@tailwindcss/postcss": "^4.1.18",
-1
public/checkout-one-time-payments.svg
··· 1 - <svg height="156" viewBox="0 0 240 156" width="240" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd" transform="translate(0 1)"><rect fill="#fff" height="154" opacity=".8" rx="4" width="240"/><path d="m99 0v154" opacity=".068034" stroke="#000" stroke-linecap="square"/><text style="opacity:.836844;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:18;font-weight:500;letter-spacing:-.45;fill:#8f6ed5"><tspan x="14" y="48">$29.90</tspan></text><path d="m18 17h23c1.1045695 0 2 .8954305 2 2s-.8954305 2-2 2h-23c-1.1045695 0-2-.8954305-2-2s.8954305-2 2-2z" fill="#8f6ed5"/><g transform="translate(112 109)"><path d="m3.58943928 0h109.02112172c1.248126 0 1.700727.12995586 2.157023.37398579s.8144.60213393 1.05843 1.05843023c.24403.45629629.373986.90889702.373986 2.15702326v12.42112142c0 1.2481263-.129956 1.700727-.373986 2.1570233s-.602134.8144003-1.05843 1.0584302-.908897.3739858-2.157023.3739858h-109.02112172c-1.24812624 0-1.70072697-.1299559-2.15702326-.3739858-.4562963-.2440299-.8144003-.6021339-1.05843023-1.0584302s-.37398579-.908897-.37398579-2.1570233v-12.42112142c0-1.24812624.12995586-1.70072697.37398579-2.15702326.24402993-.4562963.60213393-.8144003 1.05843023-1.05843023.45629629-.24402993.90889702-.37398579 2.15702326-.37398579z" fill="#8f6ed5"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:10;font-weight:500;letter-spacing:.12;fill:#f7fafc"><tspan x="29" y="14">Pay $29.90</tspan></text></g><g transform="translate(112 83)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><g fill-rule="nonzero" opacity=".5" transform="translate(95.2 4.2)"><rect fill="#ebf1f8" height="10.7" rx="1.4" stroke="#a450b5" stroke-width=".5" width="16.3" x=".25" y=".25"/><path d="m2.30141283 4.14010612c-.36619921-.20085195-.7841356-.36238968-1.25141283-.47449094l.01960035-.08729771h1.91588511c.25968832.00910242.47040833.08723662.53897699.36318047l.41637443 1.98511543.12755149.59789495 1.1661881-2.94619085h1.2591758l-1.87173549 4.31595242h-1.25924092zm5.11856981 3.75874553h-1.1908025l.7448132-4.32053418h1.19073738zm4.31676486-4.21490945-.1619471.93296757-.1077042-.04587865c-.2154736-.0873588-.4999065-.17465651-.8870297-.16543191-.4701478 0-.68099808.18834069-.68588189.3722218 0 .20226923.25506499.33556776.67155609.53325523.6860773.30795505 1.0043062.6848197.9995526 1.17665586-.0096374.89631352-.8230844 1.47550848-2.07255766 1.47550848-.53422342-.00464284-1.04878137-.11057303-1.32807004-.22994303l.16663551-.96986596.15680278.06903178c.38712314.16103343.64186253.22982085 1.11728491.22982085.34284328 0 .7104312-.13342072.7151196-.42286548 0-.18846286-.1566074-.32646533-.61731322-.53789807-.45074287-.20691207-1.05340471-.55158225-1.04363709-1.17213519.00507916-.8410881.83317755-1.42938548 2.00913331-1.42938548.4606407 0 .8331775.09652231 1.0680561.1839422zm1.5826792 2.68429772h.989785c-.0489683-.21601449-.27447-1.2502083-.27447-1.2502083l-.0832201-.37228289c-.058801.16085015-.1616214.42286547-.1566725.41364087 0 0-.3773555.96064136-.4754224 1.20885032zm1.4698958-2.78992245.9606775 4.32047309h-1.1025684s-.1078996-.49641792-.1420862-.64810457h-1.5288922c-.0442148.11484934-.2499207.64810457-.2499207.64810457h-1.2494733l1.7687848-3.96199655c.122551-.28040343.3383502-.35847654.6223924-.35847654z" fill="#8f6ed5"/></g><g fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500"><text opacity=".67"><tspan x="5.6" y="12.9">4242</tspan></text><text opacity=".67"><tspan x="33.6" y="12.9">11/26</tspan></text><text opacity=".67"><tspan x="61.6" y="12.9">934</tspan></text></g></g><g transform="translate(112 57)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">Jenny Rosen</tspan></text></g><g transform="translate(112 31)"><path d="m3.58943928.35c-1.08510563 0-1.53103464.08611287-1.99196366.33262041-.39530234.21141-.70344521.51955287-.91485521.91485521-.24650754.46092902-.33262041.90685803-.33262041 1.99196366v12.42112142c0 1.0851056.08611287 1.5310347.33262041 1.9919637.21141.3953023.51955287.7034452.91485521.9148552.46092902.2465075.90685803.3326204 1.99196366.3326204h109.02112172c1.085105 0 1.531034-.0861129 1.991963-.3326204.395303-.21141.703446-.5195529.914856-.9148552.246507-.460929.33262-.9068581.33262-1.9919637v-12.42112142c0-1.08510563-.086113-1.53103464-.33262-1.99196366-.21141-.39530234-.519553-.70344521-.914856-.91485521-.460929-.24650754-.906858-.33262041-1.991963-.33262041z" fill="#fff" opacity=".8" stroke="#8f6ed5" stroke-opacity=".266253" stroke-width=".7"/><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="8.4" font-weight="500" opacity=".67"><tspan x="5.6" y="13.6">jenny@rosen.com</tspan></text></g></g></svg>
public/checkout_demo.gif

This is a binary file and will not be displayed.

-1
public/elements-card-payment.svg
··· 1 - <svg height="64" viewBox="0 0 202 64" width="202" xmlns="http://www.w3.org/2000/svg"><g fill="none" fill-rule="evenodd"><rect fill="#fff" height="28" opacity=".8" rx="4" width="202"/><g transform="translate(0 36)"><rect fill="#4e5663" height="28" rx="4" width="202"/><text style="opacity:.7;font-family:-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;font-size:14;font-weight:500;letter-spacing:-.154;fill:#fff"><tspan x="63" y="19">Pay $29.90</tspan></text></g><text fill="#1a1f36" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif" font-size="12" font-weight="500" opacity=".797503"><tspan x="9" y="19">4242 4242 4242 4242</tspan></text><g fill-rule="nonzero" opacity=".5" transform="translate(171 6)"><rect height="15.5" rx="2" stroke="#4f566b" stroke-width=".5" width="23.5" x=".25" y=".25"/><path d="m3.28773262 5.91443732c-.52314173-.28693137-1.12019372-.51769955-1.78773262-.6778442l.0280005-.12471102h2.73697872c.37098332.01300346.67201191.12462375.76996714.51882925l.59482061 2.83587917.18221641.85413565 1.66598301-4.20884407h1.79882257l-2.67390785 6.1656463h-1.79891559zm7.31224258 5.36963648h-1.70114643l1.06401886-6.1721917h1.70105337zm6.1668069-6.02129922-.2313529 1.3328108-.1538632-.06554092c-.3078194-.12479829-.7141522-.2495093-1.2671852-.2363313-.6716398 0-.9728545.26905813-.9798314.53174543 0 .28895604.3643786.47938251.9593659.76179318.9801104.43993579 1.4347231.97831385 1.4279323 1.68093694-.0137677 1.28044789-1.1758348 2.10786929-2.9607967 2.10786929-.7631763-.0066327-1.4982591-.1579615-1.8972429-.3284901l.2380507-1.38552277.224004.09861682c.5530331.23004776.9169465.32831555 1.5961213.32831555.4897761 0 1.0149017-.19060107 1.0215995-.60409358 0-.26923267-.2237249-.46637906-.8818761-.76842582-.6439184-.29558867-1.5048639-.78797464-1.4909101-1.67447884.0072559-1.20155443 1.1902536-2.04197926 2.8701904-2.04197926.6580581 0 1.1902536.13788902 1.5257944.26277458zm2.2609703 3.83471102h1.4139786c-.0699548-.30859213-.3921-1.78601186-.3921-1.78601186l-.1188858-.5318327c-.0840015.22978594-.2308878.60409353-.2238179.59091553 0 0-.5390794 1.3723448-.6791749 1.72692903zm2.0998512-3.9856035 1.3723964 6.1721044h-1.5750977s-.1541422-.7091684-.2029803-.9258637h-2.1841317c-.0631639.1640705-.3570296.9258637-.3570296.9258637h-1.7849619l2.5268355-5.65999506c.1750729-.40057632.4833574-.51210934.889132-.51210934z" fill="#3c4257"/></g></g></svg>
public/elements_demo.gif

This is a binary file and will not be displayed.

public/logo.png

This is a binary file and will not be displayed.

public/social_card.png

This is a binary file and will not be displayed.

-3
tsconfig.json
··· 27 27 "@/components/*": [ 28 28 "app/components/*" 29 29 ], 30 - "@/config": [ 31 - "config/" 32 - ], 33 30 "@/lib/*": [ 34 31 "lib/*" 35 32 ],
-15
utils/get-stripejs.ts
··· 1 - /** 2 - * This is a singleton to ensure we only instantiate Stripe once. 3 - */ 4 - import { Stripe, loadStripe } from "@stripe/stripe-js"; 5 - 6 - let stripePromise: Promise<Stripe | null>; 7 - 8 - export default function getStripe(): Promise<Stripe | null> { 9 - if (!stripePromise) 10 - stripePromise = loadStripe( 11 - process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY as string, 12 - ); 13 - 14 - return stripePromise; 15 - }
-30
utils/stripe-helpers.ts
··· 1 - export function formatAmountForDisplay( 2 - amount: number, 3 - currency: string, 4 - ): string { 5 - let numberFormat = new Intl.NumberFormat(["en-US"], { 6 - style: "currency", 7 - currency: currency, 8 - currencyDisplay: "symbol", 9 - }); 10 - return numberFormat.format(amount); 11 - } 12 - 13 - export function formatAmountForStripe( 14 - amount: number, 15 - currency: string, 16 - ): number { 17 - let numberFormat = new Intl.NumberFormat(["en-US"], { 18 - style: "currency", 19 - currency: currency, 20 - currencyDisplay: "symbol", 21 - }); 22 - const parts = numberFormat.formatToParts(amount); 23 - let zeroDecimalCurrency: boolean = true; 24 - for (let part of parts) { 25 - if (part.type === "decimal") { 26 - zeroDecimalCurrency = false; 27 - } 28 - } 29 - return zeroDecimalCurrency ? amount : Math.round(amount * 100); 30 - }