Skip to content

Commit 09d4c45

Browse files
authored
Merge pull request #6 from calimania/hotfix/receipt
Hotfix: Created react component for receipt
2 parents 9d8d6ff + 1b1a7d9 commit 09d4c45

File tree

5 files changed

+285
-301
lines changed

5 files changed

+285
-301
lines changed

README.md

Lines changed: 60 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,83 +4,88 @@
44

55
#### Ready to Sell
66

7-
### Configuration
7+
# Markkët Space — Storefront starter (Astro)
88

9+
A small, content-first storefront template built with Astro and Strapi
910

10-
```env
11-
PUBLIC_STRAPI_URL=https://api.markket.place # Markkët CMS URL
12-
PUBLIC_STORE_SLUG=sell # Store identifier
13-
PUBLIC_POSTHOG_KEY= # Analytics key (optional)
14-
PUBLIC_URL=https://sell.markket.place # Your live site URL
15-
```
11+
It's designed as an example you can copy from, or fork the repo you, customize and deploy
12+
13+
This repo contains a minimal set of pages and components you can reuse:
14+
15+
- /blog — article index and list
16+
- /about — pages and nested pages
17+
- / — home page showing store info and product listing
18+
- /receipt — buyer confirmation page (renders session or payload)
19+
- /newsletter — newsletter landing and subscribe form
20+
21+
The checkout flow in this template is intentionally minimal:
1622

17-
## Hacktoberfest
23+
- the product UI opens a checkout modal (client side) that redirects to a payment link
24+
- The receipt page decodes a receipt payload or resolves a Stripe session via your backend
1825

26+
## Quick start
1927

28+
Prerequisites: Node.js (16+), npm or pnpm
2029

21-
### How to Contribute
30+
1. Fork or clone this repository.
31+
2. Install dependencies:
2232

23-
1. **🍴 Fork** this repository
24-
2. **📋 Pick an issue** or suggest improvements
25-
3. **🔧 Make your changes** following our guidelines
26-
4. **📤 Submit a PR** with a clear description
33+
```bash
34+
npm install
35+
```
2736

28-
### Good First Issues
37+
3. Copy environment variables (example in `.env` or your hosting configuration):
2938

30-
- 🎨 UI/UX improvements
31-
- 📱 Mobile responsiveness enhancements
32-
- 🚀 Performance optimizations
33-
- 📚 Documentation improvements
34-
- 🧪 Add tests
35-
- 🌐 Accessibility improvements
39+
```env
40+
PUBLIC_STRAPI_URL=https://api.markket.place # Markket - strapi API
41+
PUBLIC_STORE_SLUG=sell # slug identifier from markket
42+
PUBLIC_POSTHOG_KEY= # Optional analytics key
43+
PUBLIC_URL= # deployment url for canonical and sitemap
44+
```
3645

37-
### Contribution Guidelines
38-
- Keep PRs focused and atomic
39-
- Write clear commit messages
40-
- Test your changes locally
41-
- Update documentation as needed
42-
- Be respectful and inclusive
46+
4. Run the dev server:
47+
48+
```bash
49+
npm run dev
50+
```
4351

44-
## 🛠 Tech Stack
52+
Open http://localhost:3000 and browse the routes above.
4553

46-
- **Framework**: [Astro](https://astro.build) - The web framework for content-driven websites
47-
- **CMS**: [Strapi](https://strapi.io) - Flexible, open-source headless CMS
48-
- **Analytics**: [PostHog](https://posthog.com) - Product analytics platform
49-
- **Deployment**: GitHub Actions → GitHub Pages
50-
- **Styling**: Modern CSS with component-scoped styles
54+
## Routes & responsibilities
5155

52-
## 🌐 Live Demo
56+
- / (home)
57+
- Shows store metadata and product listings. Product cards provide a checkout modal or direct-to-payment link.
5358

54-
🔗 **[https://sell.markket.place](https://sell.markket.place)**
59+
- /blog
60+
- Content-driven article index and single article pages powered by Astro content collections.
5561

56-
We love contributions! Whether you're:
62+
- /about
63+
- Simple content pages, supports nested pages.
5764

58-
- 🐛 Fixing bugs
59-
- ✨ Adding features
60-
- 📝 Improving docs
61-
- 🎨 Enhancing design
62-
- 🧪 Writing tests
65+
- /receipt
66+
- Buyer confirmation page. The client script will either:
67+
- Parse a receipt payload from the URL (?receipt= encoded JSON or ?payload= base64), or
68+
- Resolve a Stripe session by POSTing { action: 'stripe.receipt', session_id } to your backend (configured via PUBLIC_STRAPI_URL or markket.config).
69+
- The page renders amounts, customer info, shipping (if present), and items.
6370

64-
All contributions are welcome! Check out our [issues](https://github.com/calimania/markket-space/issues) or create a new one
71+
- /newsletter
72+
- Newsletter landing and a SubscribeForm component (React) that posts to `/api/subscribers`.
6573

66-
## 📄 License
74+
## Checkout & Receipt notes
6775

68-
This project is open source and available under the [TSL](LICENSE)
76+
- Checkout modal: the front-end reads a product's metadata and redirects the buyer to a payment link
6977

70-
## 🏆 Credits & Recognition
78+
- Receipt resolving: After checkout, markket redirects back to `/receipt?session_id=...`
7179

72-
### 🤖 AI Development Team
80+
## Contributing
7381

74-
- **GitHub Copilot** - Primary coding assistant and project architect
75-
- **Colombian Coffee** - Advanced research and development support
76-
- **Octogatos** - Community-driven AI innovation and testing
82+
We welcome contributions. A few tips:
7783

78-
### 🛠️ Technology Stack
84+
- Keep changes small and focused
85+
- Update README and comment code where behaviors are non-obvious
86+
- Implement additional Markket features
87+
- Abstract components
7988

80-
- Built with ❤️ using [Astro](https://astro.build)
81-
- Powered by [Strapi](https://strapi.io) headless CMS
82-
- Content by [Markkët](https://de.markket.place)
83-
- Analytics by [PostHog](https://posthog.com)
84-
- Schema by [Cafecito](https://www.npmjs.com/package/cafecito)
85-
- Deployed via GitHub Actions
89+
## License
8690

91+
TSL

src/components/receipt.tsx

Lines changed: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
1+
import React, { useEffect, useState } from 'react';
2+
3+
function moneyFmt(value: any, currency = 'USD') {
4+
try {
5+
return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format((value && Number(value)) / 100 || value);
6+
} catch (e) {
7+
return String(value);
8+
}
9+
}
10+
11+
async function fetchFromSession(endpoint: string, sessionId: string) {
12+
try {
13+
const res = await fetch(endpoint, {
14+
method: 'POST',
15+
headers: { 'Content-Type': 'application/json' },
16+
body: JSON.stringify({ action: 'stripe.receipt', session_id: sessionId }),
17+
});
18+
if (!res.ok) {
19+
console.error('receipt.client: server returned', res.status);
20+
return null;
21+
}
22+
const json = await res.json().catch(() => null);
23+
return json?.data?.link?.response || json?.data || null;
24+
} catch (e) {
25+
console.error('receipt.client: failed to fetch receipt from session', e);
26+
return null;
27+
}
28+
}
29+
30+
/**
31+
* Read a stripe session_id, or order_id and display information
32+
*
33+
* @param props.api markket.api_url - api.markket.place
34+
* @returns
35+
*/
36+
const ReceiptComponentPage= ({ api }: { api: string }) => {
37+
const [data, setData] = useState<any | null>(null);
38+
const [loading, setLoading] = useState(false);
39+
const [error, setError] = useState<string | null>(null);
40+
41+
useEffect(() => {
42+
let mounted = true;
43+
(async () => {
44+
setLoading(true);
45+
try {
46+
const endpoint = new URL('/api/markket', api).toString();
47+
const qs = new URLSearchParams(window.location.search);
48+
const sessionId = (qs.get('session_id') || qs.get('session') || qs.get('sid') || '').trim();
49+
50+
if (!sessionId) {
51+
// no session id present — nothing to fetch
52+
if (mounted) {
53+
setData(null);
54+
setError(null);
55+
}
56+
return;
57+
}
58+
59+
const resolved = await fetchFromSession(endpoint, sessionId);
60+
61+
if (mounted) {
62+
if (resolved) setData(resolved);
63+
else setError('No receipt data returned from server for this session.');
64+
}
65+
} catch (e: any) {
66+
console.error(e);
67+
if (mounted) setError(String(e?.message || e));
68+
} finally {
69+
if (mounted) setLoading(false);
70+
}
71+
})();
72+
73+
return () => { mounted = false; };
74+
}, []);
75+
76+
const getCurrency = (d: any) => (d?.currency || d?.currency_code || (d?.link?.response && d.link.response.currency) || 'USD').toUpperCase();
77+
const getTotal = (d: any) => d?.amount_total ?? d?.amountTotal ?? d?.total ?? d?.amount ?? 0;
78+
const getSubtotal = (d: any) => d?.amount_subtotal ?? d?.amountSubtotal ?? d?.subtotal ?? 0;
79+
const getCustomerEmail = (d: any) => d?.customer_details?.email || d?.customer_email || d?.email || d?.receipt_email || '';
80+
const getCustomerName = (d: any) => d?.customer_details?.name || d?.customer || '';
81+
const getOrderId = (d: any) => d?.id || d?.session_id || d?.transaction || d?.payment_intent || '';
82+
const getCreated = (d: any) => d?.created ?? d?.created_at ?? null;
83+
const getShipping = (d: any) => d?.shipping_details || d?.shipping || d?.collected_information?.shipping_details || d?.link?.response?.shipping_details || null;
84+
85+
return (
86+
<main className="container my-12">
87+
<section className="mx-auto max-w-3xl rounded-lg p-6 bg-surface border" role="region" aria-labelledby="receipt-heading">
88+
<h2 id="receipt-heading" className="text-2xl font-semibold">Order receipt</h2>
89+
<p className="text-sm mt-1 text-muted">This page decodes a receipt payload from the URL and renders a printable copy for your records.</p>
90+
91+
{!loading && !data && !error && (
92+
<div id="receipt-empty" className="mt-6">
93+
<p className="text-muted">No receipt data detected. Append <code>?receipt=</code> followed by a URL-encoded JSON object to the URL (example in console).</p>
94+
</div>
95+
)}
96+
97+
{loading && (
98+
<div className="mt-6 text-sm text-muted">Fetching receipt…</div>
99+
)}
100+
101+
{error && (
102+
<div className="mt-6 text-sm text-red-600">Error loading receipt: {error}</div>
103+
)}
104+
105+
{data && (
106+
<div id="receipt" className="mt-6" aria-live="polite">
107+
<div className="flex justify-between items-start">
108+
<div>
109+
<p className="text-sm text-muted">Order</p>
110+
<h3 id="order-id" className="text-lg font-medium">#{getOrderId(data) || '—'}</h3>
111+
<p id="order-date" className="text-sm text-muted mt-1">{(function(){
112+
const ts = getCreated(data); try { if (!ts) return '—'; const d = (typeof ts === 'number' && String(ts).length === 10) ? new Date(ts * 1000) : new Date(ts); return d.toLocaleString(); } catch(e){return '—';}
113+
})()}</p>
114+
115+
<p className="text-sm text-muted mt-4">Billed to</p>
116+
<p id="order-email" className="font-medium break-words">{getCustomerName(data) || getCustomerEmail(data) || '—'}</p>
117+
<p id="order-shipping" className="text-sm mt-2 text-muted whitespace-pre-line break-words">{(function(){
118+
const ship = getShipping(data); if (!ship) return '—'; const lines: string[] = []; if (ship.name) lines.push(String(ship.name)); const addr = ship.address || ship; if (addr?.line1) lines.push(String(addr.line1)); if (addr?.line2) lines.push(String(addr.line2)); const cityParts = [addr?.city, addr?.state, addr?.postal_code].filter(Boolean).join(', '); if (cityParts) lines.push(cityParts); if (addr?.country) lines.push(String(addr.country)); return lines.join('\n');
119+
})()}</p>
120+
</div>
121+
</div>
122+
123+
<div className="mt-6 border-t pt-4">
124+
<h4 className="text-sm font-semibold">Items</h4>
125+
<ul id="order-items" className="mt-2 space-y-3">
126+
{(function(){
127+
const items = data.items || data.line_items || data.line_items?.data || [];
128+
if (!Array.isArray(items) || items.length === 0) return null;
129+
return items.map((it: any, i: number) => {
130+
const title = it.name || it.description || 'Item';
131+
const qty = it.qty ?? it.quantity ?? 1;
132+
const p = Number(it.unit_price ?? it.price ?? it.amount ?? 0);
133+
const currency = getCurrency(data);
134+
const price = (() => { try { return new Intl.NumberFormat(undefined, { style: 'currency', currency }).format((isNaN(p) ? 0 : p) / 100); } catch (e) { return String((isNaN(p) ? 0 : p) / 100); } })();
135+
return (
136+
<li key={i} className="flex justify-between items-center">
137+
<div className="font-medium">{title}<div className="text-sm">qty: {qty}</div></div>
138+
<div className="font-medium">{price}</div>
139+
</li>
140+
);
141+
});
142+
})()}
143+
</ul>
144+
</div>
145+
146+
<div className="mt-6 border-t pt-4 flex justify-end gap-6">
147+
<div>
148+
<p className="text-sm text-muted">Subtotal</p>
149+
<p id="order-subtotal" className="font-medium">{moneyFmt(getSubtotal(data), getCurrency(data))}</p>
150+
</div>
151+
<div>
152+
<p className="text-sm text-muted">Total</p>
153+
<p id="order-total" className="text-xl font-bold">{moneyFmt(getTotal(data), getCurrency(data))}</p>
154+
</div>
155+
</div>
156+
157+
</div>
158+
)}
159+
</section>
160+
</main>
161+
);
162+
};
163+
164+
export default ReceiptComponentPage;

src/layouts/PageLayout.astro

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export interface Props {
1212
heroLqip?: string;
1313
heroAspect?: string; // CSS aspect like "4/5" or "16/9"
1414
heroVariant?: "spacious" | "compact";
15+
heroSize?: "spacious" | "compact" | "small";
1516
heroCtaText?: string;
1617
heroCtaUrl?: string;
1718
}
@@ -28,11 +29,17 @@ const heroImage = props.heroImage ?? image ?? "";
2829
const heroLqip = props.heroLqip ?? undefined;
2930
const heroAspect = props.heroAspect ?? "4/5";
3031
const heroVariant = (props.heroVariant ?? "spacious") as "spacious" | "compact";
32+
const heroSize = (props.heroSize ?? undefined) as
33+
| "spacious"
34+
| "compact"
35+
| "small"
36+
| undefined;
3137
const heroCtaText = props.heroCtaText ?? undefined;
3238
const heroCtaUrl = props.heroCtaUrl ?? undefined;
3339
3440
// Helper at runtime to pick the variant class (keeps TS happy)
3541
const isCompact = heroVariant === "compact";
42+
const isSmall = heroSize === "small";
3643
---
3744

3845
<BaseLayout title={title} description={description} image={image}>
@@ -42,7 +49,7 @@ const isCompact = heroVariant === "compact";
4249
{
4350
heroTitle || heroSubtitle || heroImage ? (
4451
<section
45-
class={`page-hero ${isCompact ? "hero-compact" : "hero-spacious"}`}
52+
class={`page-hero ${isSmall ? "hero-small" : isCompact ? "hero-compact" : "hero-spacious"}`}
4653
aria-labelledby="page-hero-title"
4754
>
4855
<div class="page-hero-content">
@@ -135,6 +142,26 @@ const isCompact = heroVariant === "compact";
135142
transform-origin: center center;
136143
}
137144

145+
/* small hero variant for informational pages */
146+
.page-hero.hero-small {
147+
padding: 1rem 0 0.75rem;
148+
background: var(--bg-alt);
149+
}
150+
.page-hero.hero-small .page-hero-content {
151+
max-width: 900px;
152+
grid-template-columns: 1fr 260px;
153+
gap: 1rem;
154+
padding: 0 1rem;
155+
}
156+
.page-hero.hero-small .hero-copy h1 {
157+
font-size: clamp(1.2rem, 2.2vw, 1.6rem);
158+
margin-bottom: 0.25rem;
159+
}
160+
.page-hero.hero-small .about-subtitle {
161+
font-size: 0.95rem;
162+
margin-bottom: 0.5rem;
163+
}
164+
138165
.hero-image .img-frame img {
139166
width: 100%;
140167
height: 100%;

0 commit comments

Comments
 (0)