Skip to content

Commit fa79b0a

Browse files
authored
Merge pull request #4 from calimania/feat/content-entries
Feat: Displaying entries in /blog /about /products
2 parents ec244ee + ae78502 commit fa79b0a

File tree

13 files changed

+1367
-65
lines changed

13 files changed

+1367
-65
lines changed

src/components/BlocksContent.astro

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -214,13 +214,13 @@ const { content, className = "" } = Astro.props;
214214
.filter((child) => child.type === "list-item")
215215
.map((child) => (
216216
<li class="leading-relaxed">
217-
{child.children?.map((grandChild) => (
217+
{child.children?.map((grandChild: any) => (
218218
<Fragment>
219219
{grandChild.type === "link" ? (
220220
<a
221221
href={grandChild.url}
222222
target={
223-
grandChild.url?.startsWith("/")
223+
(grandChild.url || "").startsWith("/")
224224
? "_self"
225225
: "_blank"
226226
}
@@ -273,16 +273,24 @@ const { content, className = "" } = Astro.props;
273273
<div class="rounded-lg overflow-hidden shadow-lg">
274274
<img
275275
src={block.image.url}
276-
alt={block.image.alternativeText || ""}
276+
alt={
277+
(block.image as any).alternativeText ||
278+
(block.image as any).name ||
279+
""
280+
}
277281
class="w-full h-auto object-cover"
278282
width={block.image.width}
279283
height={block.image.height}
280284
loading="lazy"
281285
/>
282286
</div>
283-
{(block.image.caption || block.image.alternativeText) && (
287+
{((block.image as any).caption ||
288+
(block.image as any).alternativeText ||
289+
(block.image as any).name) && (
284290
<figcaption class="text-center text-sm text-gray-500 mt-3 italic">
285-
{block.image.caption || block.image.alternativeText}
291+
{(block.image as any).caption ||
292+
(block.image as any).alternativeText ||
293+
(block.image as any).name}
286294
</figcaption>
287295
)}
288296
</figure>

src/components/checkoutModal.tsx

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
import { type FC, useEffect, useState } from "react";
2+
import { createPaymentLink } from "../scripts/ui.product";
3+
4+
// Loose types: CMS shapes vary between projects
5+
type AnyPrice = any;
6+
type AnyOptions = any;
7+
8+
interface Props {
9+
prices: AnyPrice[];
10+
product: any;
11+
store: any;
12+
hideTrigger?: boolean;
13+
}
14+
15+
const CheckoutModal: FC<Props> = ({ prices, product, store, hideTrigger = false }) => {
16+
const [isModalOpen, setIsModalOpen] = useState(false);
17+
const [selectedPriceId, setSelectedPriceId] = useState("");
18+
const [quantity, setQuantity] = useState(1);
19+
const [tip, setTip] = useState(0);
20+
const [total, setTotal] = useState(0);
21+
const [selectedPrice, setSelectedPrice] = useState({} as AnyPrice);
22+
const [submitting, setSubmitting] = useState(false);
23+
const [serverError, setServerError] = useState<string | null>(null);
24+
25+
const [options, setOptions] = useState({
26+
totalPrice: 0,
27+
product: product?.id || product?.SKU || product?.slug || product?.Name,
28+
prices: [],
29+
stripe_test: !!(product?.Name || product?.Title || "").match(/test/i),
30+
includes_shipping: !/(digital)/i.test(product?.Name || product?.Title || ""),
31+
store_id: store?.data?.documentId || store?.documentId || store?.id || store?.slug,
32+
} as AnyOptions);
33+
34+
useEffect(() => {
35+
const price = prices.find((p: any) => p.STRIPE_ID === selectedPriceId) as AnyPrice;
36+
const basePrice = parseInt(String(price?.Price || price?.price || '0'), 10);
37+
const subtotal = basePrice * quantity;
38+
const newTotal = subtotal + tip;
39+
40+
const option_prices: AnyPrice[] = [
41+
{
42+
quantity,
43+
price: selectedPriceId,
44+
currency: (price?.Currency || price?.currency || 'usd'),
45+
},
46+
];
47+
48+
if (tip > 0) {
49+
option_prices.push({
50+
unit_amount: String(tip),
51+
currency: 'usd',
52+
product: product?.SKU || product?.slug || product?.Name,
53+
});
54+
}
55+
56+
setOptions((prev: AnyOptions) => ({
57+
...prev,
58+
totalPrice: newTotal,
59+
prices: option_prices,
60+
}));
61+
62+
setTotal(Number(newTotal));
63+
setSelectedPrice(price as AnyPrice);
64+
}, [selectedPriceId, quantity, tip]);
65+
66+
const redirectToPaymentLink = async (e: any) => {
67+
e.preventDefault();
68+
setSubmitting(true);
69+
setServerError(null);
70+
try {
71+
const link = await createPaymentLink(options);
72+
console.log("Payment link:", link);
73+
} catch (err: any) {
74+
setServerError(err?.message || String(err));
75+
} finally {
76+
setSubmitting(false);
77+
}
78+
};
79+
80+
useEffect(() => {
81+
const handleEsc = (event: KeyboardEvent) => {
82+
if (event.key === 'Escape') {
83+
setIsModalOpen(false);
84+
}
85+
};
86+
87+
if (isModalOpen) {
88+
window.addEventListener('keydown', handleEsc);
89+
}
90+
91+
return () => {
92+
window.removeEventListener('keydown', handleEsc);
93+
};
94+
}, [isModalOpen]);
95+
96+
return (
97+
<>
98+
{!hideTrigger && (
99+
<button
100+
className="w-full mb-5 flex items-center justify-center rounded-lg bg-sky-600 px-6 py-3 text-base font-semibold text-white hover:bg-sky-700 transition"
101+
onClick={() => setIsModalOpen(true)}
102+
>
103+
Available Options and Prices
104+
</button>
105+
)}
106+
{isModalOpen && (
107+
<div
108+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm"
109+
onClick={(e) =>
110+
(e.target as Element).classList.contains("fixed") &&
111+
setIsModalOpen(false)
112+
}
113+
>
114+
<div className="relative w-full max-w-md modal-panel p-6 shadow-xl ">
115+
<button
116+
className="absolute top-4 right-4 text-gray-400 hover:text-gray-700 dark:hover:text-white transition"
117+
onClick={() => setIsModalOpen(false)}
118+
>
119+
<svg
120+
xmlns="http://www.w3.org/2000/svg"
121+
width="22"
122+
height="22"
123+
viewBox="0 0 24 24"
124+
stroke="currentColor"
125+
fill="none"
126+
strokeWidth="2"
127+
strokeLinecap="round"
128+
strokeLinejoin="round"
129+
>
130+
<path d="M18 6L6 18M6 6l12 12" />
131+
</svg>
132+
</button>
133+
<form onSubmit={redirectToPaymentLink}>
134+
<h2 className="text-xl font-semibold text-gray-200 mb-1">
135+
Checkout: {product?.Name || product?.Title}
136+
</h2>
137+
<p className="text-sm text-gray-300 mb-4">
138+
Select your preferred option and customize your order.
139+
</p>
140+
<div className="mb-4">
141+
<label
142+
htmlFor="price"
143+
className="block text-sm font-medium mb-1"
144+
>
145+
Product Options
146+
</label>
147+
<select
148+
id="price"
149+
name="price"
150+
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-base focus:ring-sky-500 focus:border-sky-500"
151+
onChange={(e) => setSelectedPriceId(e.target.value)}
152+
>
153+
<option value="">Select an option</option>
154+
{prices.map((price: any) => (
155+
<option key={price.STRIPE_ID} value={price.STRIPE_ID}>
156+
{(price.Name || price.name || '').replace(/_/g, ' ')} — ${price.Price || price.price}{' '}
157+
{price.Currency || price.currency}
158+
</option>
159+
))}
160+
</select>
161+
</div>
162+
<div className="mb-4">
163+
<label
164+
htmlFor="quantity"
165+
className="block text-sm font-medium mb-1"
166+
>
167+
Quantity
168+
</label>
169+
<input
170+
type="number"
171+
id="quantity"
172+
value={quantity}
173+
onChange={(e) => setQuantity(Number(e.target.value))}
174+
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-base focus:ring-sky-500 focus:border-sky-500"
175+
/>
176+
</div>
177+
<div className="mb-4">
178+
<label
179+
htmlFor="tip"
180+
className="block text-sm font-medium mb-1"
181+
>
182+
Tip / Custom Price
183+
</label>
184+
<input
185+
type="number"
186+
id="tip"
187+
value={tip}
188+
onChange={(e) => setTip(Number(e.target.value))}
189+
className="w-full rounded-md border border-gray-300 bg-white px-3 py-2 text-base focus:ring-sky-500 focus:border-sky-500"
190+
/>
191+
</div>
192+
<div className="mb-4">
193+
<label className="block text-sm font-medium mb-1">Total</label>
194+
<p className="text-2xl font-semibold">${total || 0}</p>
195+
</div>
196+
<div className="mb-4">
197+
<button
198+
type="submit"
199+
disabled={submitting || total <= 0 || !selectedPrice?.Description || quantity < 1}
200+
className="w-full rounded-lg bg-sky-600 px-5 py-3 text-white font-semibold hover:bg-sky-700 transition disabled:opacity-50 disabled:cursor-not-allowed"
201+
>
202+
{submitting ? 'Processing...' : 'Continue to Payment'}
203+
</button>
204+
</div>
205+
<div className="text-sm text-gray-600 dark:text-gray-400">
206+
{total > 0
207+
? selectedPrice?.Description || "Continue using custom price"
208+
: "Please select an option and enter a valid total."}
209+
</div>
210+
{serverError && <div className="text-sm text-red-600 mt-3">{serverError}</div>}
211+
</form>
212+
</div>
213+
</div>
214+
)}
215+
</>
216+
);
217+
};
218+
219+
export default CheckoutModal;

src/content/config.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function strapiLoader(query: StrapiQueryOptions) {
2828
name: `strapi-${query.contentType}`,
2929
schema: async () => await fetchStrapiSchema(query.contentType, config.api_url),
3030

31-
async load({ store, logger, meta }) {
31+
async load({ store, logger, meta }: { store: any; logger: any; meta: any }) {
3232
const lastSynced = meta.get("lastSynced");
3333
if (lastSynced && Date.now() - Number(lastSynced) < config.sync_interval) {
3434
logger.info("Skipping sync");
@@ -47,7 +47,7 @@ function strapiLoader(query: StrapiQueryOptions) {
4747
const pages = defineCollection({
4848
loader: strapiLoader({
4949
contentType: "page",
50-
sort: 'slug:DESC',
50+
sort: 'slug:ASC',
5151
filter: `filters[store][slug][$eq]=${config.store_slug}`,
5252
populate: 'SEO.socialImage,albums,albums.tracks,albums.cover'
5353
}) as Loader,

src/layouts/LandingLayout.astro

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,6 @@ const {
194194
)
195195
}
196196

197-
<!-- CTA Section -->
198197
<section class="final-cta-section">
199198
<div class="container">
200199
<div class="cta-content">
@@ -211,13 +210,7 @@ const {
211210
>
212211
Get Started for Free
213212
</a>
214-
<a
215-
href="https://github.com/calimania/markket-space"
216-
class="cta-secondary"
217-
target="_blank"
218-
>
219-
View Documentation
220-
</a>
213+
<a href="/blog" class="cta-secondary">Blog</a>
221214
</div>
222215
</div>
223216
</div>

src/lib/entry.utils.ts

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Utility helpers for posts: excerpt, image selection, related picks
2+
export function renderExcerpt(post: any): string {
3+
const meta = post.data?.SEO || {};
4+
let excerpt = "";
5+
const blocks = post.data?.Content || [];
6+
for (const b of blocks) {
7+
if (b?.type === "paragraph" && Array.isArray(b.children)) {
8+
const t = b.children.map((c: any) => c?.text || "").join(" ").trim();
9+
if (t) { excerpt = t; break; }
10+
}
11+
}
12+
return excerpt || meta?.metaDescription || "";
13+
}
14+
15+
export function imgFor(post: any): string {
16+
const meta = post.data?.SEO || {};
17+
return (
18+
meta?.socialImage?.formats?.large?.url ||
19+
meta?.socialImage?.formats?.small?.url ||
20+
meta?.socialImage?.url ||
21+
post.data?.cover?.url ||
22+
""
23+
);
24+
}
25+
26+
export function getTags(post: any): string[] {
27+
return (post.data?.Tags || post.data?.tags || [])
28+
.map((t: any) => (typeof t === "string" ? t.toLowerCase() : (t?.name || "").toLowerCase()))
29+
.filter(Boolean);
30+
}
31+
32+
export function titleWords(post: any): string[] {
33+
const t = (post.data?.Title || post.data?.title || "").toLowerCase();
34+
return Array.from(new Set(t.split(/[^a-z0-9]+/).filter(Boolean)));
35+
}
36+
37+
export function daysSince(dateLike: any): number {
38+
const d = new Date(dateLike || 0).getTime();
39+
if (!d || isNaN(d)) return 365 * 10;
40+
return (Date.now() - d) / (1000 * 60 * 60 * 24);
41+
}
42+
43+
export function pickRelated(allPosts: any[], current: any, limit = 3) {
44+
const candidates = allPosts.filter((p) => p.id !== current.id);
45+
const tagsA = getTags(current);
46+
47+
const scored = candidates.map((c) => {
48+
const tagsB = getTags(c);
49+
const tagOverlap = tagsA.filter((t: string) => tagsB.includes(t)).length;
50+
51+
const wordsA = titleWords(current);
52+
const wordsB = titleWords(c);
53+
const titleOverlap = wordsA.filter((w: string) => wordsB.includes(w)).length;
54+
55+
const days = daysSince((c.data as any)?.Date || (c.data as any)?.date || (c.data as any)?.publishedAt);
56+
const recency = Math.max(0, 1 - days / 365);
57+
58+
const score = tagOverlap * 10 + titleOverlap * 2 + recency * 1;
59+
return { post: c, score };
60+
});
61+
62+
return scored.sort((a, b) => b.score - a.score).slice(0, limit).map((r) => r.post);
63+
}

0 commit comments

Comments
 (0)