-
Notifications
You must be signed in to change notification settings - Fork 29
feat(blog): add shoppable product block sections #1603
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
yuriassuncx
wants to merge
54
commits into
deco-cx:main
Choose a base branch
from
yuriassuncx:feat/autonomous-blog-compatibility
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
54 commits
Select commit
Hold shift + click to select a range
ce4570f
feat: add Spire webhook integration for Blog CMS
yuriassuncx 10bf7c3
feat: implement Spire webhook for post synchronization and add Pendin…
yuriassuncx 03b88f7
feat: secure and URL-encode SpireDashboard embedded iframe origin and…
yuriassuncx 7ccde83
style: auto-format and optimize all files in the spire module with de…
yuriassuncx c4af58c
style: auto-format all files in the blog module with deno fmt
yuriassuncx d0fbc96
feat: implement Deco-Spire quick approvals action and bidirectional m…
yuriassuncx 1585aeb
feat: enhance webhook token verification to support query param auth
yuriassuncx 210abec
feat(blog): implement HMAC-SHA256 signature verification for webhook
yuriassuncx f3f4886
refactor(webhook): rename importSpirePost action to a generic standar…
yuriassuncx fa2939e
feat(webhook): secure parsing, validation whitelists, request timeout…
yuriassuncx 33603e2
feat: add Spire ↔ Deco bidirectional integration
yuriassuncx 9ff93c4
fix(blog): authenticate syncAllPosts via webhook secret from ctx
yuriassuncx 7d206e1
fix(blog): security, HTML validity and error handling hardening
yuriassuncx b2434d3
fix(blog): write blocks in Deco-native URL-encoded format with name f…
yuriassuncx 92e61e4
fix(blog): correct date format and spireWarning type for Deco admin
yuriassuncx cfc8b3b
feat(blog): extract syncBlogPosts util and add auto startup/cron sync
yuriassuncx 06b9e2d
feat(blog, spire): integrate Spire API directly into blog loaders
yuriassuncx 0b1fc05
feat(blog): restore block writes for Studio CMS visibility
yuriassuncx 9de04ff
feat(blog): auto-sync Spire posts to Studio CMS on startup
yuriassuncx 042634b
fix(blog): handle HttpError from fetchSafe in Spire API loaders
yuriassuncx 6a39e62
fix: address code review findings — security, cache, data integrity
yuriassuncx 7bcad36
fix(SpireSyncPreviewTab): restore full webhook URL template
yuriassuncx 9b306d3
feat(blog): add hourly Deno.cron reconciliation for Spire post updates
yuriassuncx 36ca682
fix(spireImport): improve JSON parse error context and add string-lis…
yuriassuncx 9c1c41f
fix(blog/mod): resolve double-sync, HMR restart, and loader hang
yuriassuncx b233435
feat(blog): add 30-min setInterval fallback for local dev reconciliation
yuriassuncx 37a813a
refactor(blog): proper TypeScript patterns + security + full audit
yuriassuncx dede921
feat(blog): enhance Spire post integration and improve content fetchi…
yuriassuncx f4bfe88
merge(main): resolve conflicts, fix blog app bugs and refactor mod.ts
yuriassuncx b376be8
refactor(resolveSpireGate): remove unused request authorization and c…
yuriassuncx 231beff
feat(blog+spire): Spire sections migrated to blog as source of truth;…
yuriassuncx bfe28aa
fix(blog+spire): deno task bundle regenerated manifests + spire block…
yuriassuncx 9214f0d
fix(Comparison): remove conflicting text-base alongside text-xs (same…
yuriassuncx d7a84fa
feat(blog): delete spire app — blog is now the single app
yuriassuncx b48e082
feat(blog): add product block sections + rename Cta→CallToAction, rem…
yuriassuncx 5a3c31d
merge: resolve conflicts with origin/main (keep product block section…
yuriassuncx b64491f
fix(blog): regenerate manifest.gen.ts with correct section/loader ord…
yuriassuncx ee381fb
refactor(blog): remove spireWebhookSecret from State type definition
yuriassuncx 72d930d
feat(blog): add spireWebhookSecret to blog app State
yuriassuncx dd18b56
chore(blog): remove spireWebhookSecret from State — stored in sites.m…
yuriassuncx bc33060
refactor(blog): remove SpireBlogPost interface + dead blocksToSection…
yuriassuncx 7bb820c
refactor(blog): minimal Spire integration — revert non-essential changes
yuriassuncx 8c59bd0
fix(blog): revert logo url + exclude ui-templates from deno fmt
yuriassuncx 73bd4ea
ci: retrigger after esm.sh 522 transient failure
yuriassuncx 71cdcc7
fix(ci): move ui-templates exclusion to fmt.exclude only
yuriassuncx 8b10435
refactor(blog): remove spireBlogSlug from State type
yuriassuncx 7980bd6
feat(blog): implement dynamic product selection and loading functiona…
yuriassuncx 457336b
fix(blog): format product resolver and product block files to satisfy CI
yuriassuncx 9b30255
refactor(blog): consolidate product blocks into blog-only resolver
yuriassuncx 178b42b
fix(blog): address review findings for product blocks and loaders
yuriassuncx b741c87
feat(blog): add shoppable product block sections
yuriassuncx f36063c
fix(blog): skip product resolution when shelf has no refs
yuriassuncx 31cacb9
feat(blog): hybrid product input coercion for block loaders
yuriassuncx 55e7cc7
fix(blog): detect Shopify when VTEX invoke is absent
yuriassuncx File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,25 @@ | ||
| import { allowCorsFor } from "@deco/deco"; | ||
| import { searchProductOptions } from "../../utils/productResolver.ts"; | ||
| import { AppContext } from "../../mod.ts"; | ||
|
|
||
| export interface Props { | ||
| term?: string; | ||
| } | ||
|
|
||
| /** | ||
| * @title Products by term | ||
| * @description Searchable product picker for blog product blocks. | ||
| */ | ||
| export default async function productsByTerm( | ||
| props: Props, | ||
| req: Request, | ||
| ctx: AppContext, | ||
| ) { | ||
| Object.entries(allowCorsFor(req)).forEach(([name, value]) => { | ||
| ctx.response.headers.set(name, value); | ||
| }); | ||
|
|
||
| return await searchProductOptions(props.term ?? "", req, ctx); | ||
| } | ||
|
|
||
| export const cache = "no-cache"; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import Image from "../../../website/components/Image.tsx"; | ||
| import { getProductDisplay } from "../../utils/productData.ts"; | ||
| import { coerceProduct } from "../../utils/coerceProductInput.ts"; | ||
| import { sanitizeHtml } from "../../utils/sanitizeHtml.ts"; | ||
| import { AppContext } from "../../mod.ts"; | ||
| import type { Product } from "../../../commerce/types.ts"; | ||
|
|
||
| /** | ||
| * @title Product | ||
| * @description Search and select a storefront product dynamically. | ||
| * @format dynamic-options | ||
| * @options blog/loaders/options/productsByTerm.ts | ||
| */ | ||
| type ProductReference = string; | ||
|
|
||
| export interface Props { | ||
| /** | ||
| * @title Product | ||
| * @description Product reference as `platform:kind:id` (e.g. `vtex:product:123`). Loader-ref shapes work at runtime but strings are preferred. | ||
| * @format dynamic-options | ||
| * @options blog/loaders/options/productsByTerm.ts | ||
| */ | ||
| product?: ProductReference; | ||
| /** Badge text (e.g. "New", "Sale") */ | ||
| badge?: string; | ||
| /** Short description (HTML) */ | ||
| description?: string; | ||
| /** Call-to-action button text */ | ||
| cta?: string; | ||
| } | ||
|
|
||
| type RuntimeProps = Omit<Props, "product"> & { | ||
| product: Product | null; | ||
| }; | ||
|
|
||
| export async function loader(props: Props, req: Request, ctx: AppContext) { | ||
| const product = await coerceProduct(props.product, req, ctx); | ||
| return { | ||
| badge: props.badge, | ||
| description: props.description, | ||
| cta: props.cta, | ||
| product, | ||
| } as RuntimeProps; | ||
| } | ||
|
|
||
| /** | ||
| * @title Product Card | ||
| * @description Displays a shoppable product card with image, name, price and CTA. | ||
| */ | ||
| export default function ProductCard( | ||
| { product, badge, description, cta }: RuntimeProps, | ||
| ) { | ||
| if (!product) return null; | ||
|
|
||
| const { | ||
| name, | ||
| imageUrl, | ||
| width, | ||
| height, | ||
| price, | ||
| listPrice, | ||
| safeUrl, | ||
| isExternal, | ||
| } = getProductDisplay(product); | ||
| if (!name) return null; | ||
|
|
||
| const productDescription = description ?? product.description; | ||
|
|
||
| return ( | ||
| <div class="not-prose my-8 border border-line rounded-brand overflow-hidden bg-base max-w-xs mx-auto"> | ||
| <div class="relative overflow-hidden bg-alt aspect-square"> | ||
| <Image | ||
| src={imageUrl} | ||
| alt={name} | ||
| width={width} | ||
| height={height} | ||
| fit="cover" | ||
| loading="lazy" | ||
| fetchPriority="low" | ||
| class="w-full h-full object-cover block" | ||
| /> | ||
| {badge && ( | ||
| <span class="absolute top-3 left-3 text-xs font-semibold px-2 py-1 rounded-full bg-accent text-inverted leading-none"> | ||
| {badge} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <div class="p-5 flex flex-col gap-2"> | ||
| <h3 class="text-[1.0625rem] font-semibold leading-snug m-0">{name}</h3> | ||
| {productDescription && ( | ||
| <div | ||
| class="text-sm text-secondary leading-normal [text-wrap:pretty] line-clamp-3" | ||
| dangerouslySetInnerHTML={{ | ||
| __html: sanitizeHtml(productDescription), | ||
| }} | ||
| /> | ||
| )} | ||
| <div class="mt-1 flex items-baseline gap-2"> | ||
| {price && ( | ||
| <span class="text-lg font-bold text-accent tabular-nums"> | ||
| {price} | ||
| </span> | ||
| )} | ||
| {listPrice && listPrice !== price && ( | ||
| <span class="text-sm text-muted line-through tabular-nums"> | ||
| {listPrice} | ||
| </span> | ||
| )} | ||
| </div> | ||
| {safeUrl && ( | ||
| <a | ||
| href={safeUrl} | ||
| {...(isExternal | ||
| ? { target: "_blank", rel: "noopener noreferrer" } | ||
| : {})} | ||
| class="mt-2 inline-flex items-center justify-center py-[0.6em] px-[1.5em] rounded-full bg-accent text-inverted font-semibold text-sm tracking-[0.02em] no-underline transition-opacity hover:opacity-85" | ||
| > | ||
| {cta ?? "Comprar"} | ||
| </a> | ||
| )} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function LoadingFallback() { | ||
| return ( | ||
| <div class="not-prose my-8 border border-line rounded-brand bg-base max-w-xs mx-auto p-5 animate-pulse"> | ||
| <div class="aspect-square bg-alt mb-4 rounded-brand" /> | ||
| <div class="h-5 bg-alt mb-2 rounded" /> | ||
| <div class="h-4 bg-alt mb-2 rounded" /> | ||
| <div class="h-4 bg-alt w-2/3 mb-4 rounded" /> | ||
| <div class="h-9 bg-alt w-1/2 rounded-full" /> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,143 @@ | ||
| import Image from "../../../website/components/Image.tsx"; | ||
| import { getProductDisplay } from "../../utils/productData.ts"; | ||
| import { coerceProduct } from "../../utils/coerceProductInput.ts"; | ||
| import { sanitizeHtml } from "../../utils/sanitizeHtml.ts"; | ||
| import { AppContext } from "../../mod.ts"; | ||
| import type { Product } from "../../../commerce/types.ts"; | ||
|
|
||
| /** | ||
| * @title Product | ||
| * @description Search and select a storefront product dynamically. | ||
| * @format dynamic-options | ||
| * @options blog/loaders/options/productsByTerm.ts | ||
| */ | ||
| type ProductReference = string; | ||
|
|
||
| export interface Props { | ||
| /** | ||
| * @title Product | ||
| * @description Product reference as `platform:kind:id` (e.g. `vtex:product:123`). Loader-ref shapes work at runtime but strings are preferred. | ||
| * @format dynamic-options | ||
| * @options blog/loaders/options/productsByTerm.ts | ||
| */ | ||
| product?: ProductReference; | ||
| /** Badge text (e.g. "Destaque", "Limited") */ | ||
| badge?: string; | ||
| /** Full description (HTML) */ | ||
| description?: string; | ||
| /** Call-to-action button text */ | ||
| cta?: string; | ||
| } | ||
|
|
||
| type RuntimeProps = Omit<Props, "product"> & { | ||
| product: Product | null; | ||
| }; | ||
|
|
||
| export async function loader(props: Props, req: Request, ctx: AppContext) { | ||
| const product = await coerceProduct(props.product, req, ctx); | ||
| return { | ||
| badge: props.badge, | ||
| description: props.description, | ||
| cta: props.cta, | ||
| product, | ||
| } as RuntimeProps; | ||
| } | ||
|
|
||
| /** | ||
| * @title Product Highlight | ||
| * @description Full-width featured product block with image and details side by side. | ||
| */ | ||
| export default function ProductHighlight( | ||
| { product, badge, description, cta }: RuntimeProps, | ||
| ) { | ||
| if (!product) return null; | ||
|
|
||
| const { | ||
| name, | ||
| imageUrl, | ||
| width, | ||
| height, | ||
| price, | ||
| listPrice, | ||
| safeUrl, | ||
| isExternal, | ||
| } = getProductDisplay(product); | ||
| if (!name) return null; | ||
|
|
||
| const productDescription = description ?? product.description; | ||
|
|
||
| return ( | ||
| <div class="not-prose my-10 border border-line rounded-brand overflow-hidden bg-base"> | ||
| <div class="flex flex-col sm:flex-row"> | ||
| <div class="relative sm:w-2/5 overflow-hidden bg-alt aspect-video sm:aspect-auto sm:min-h-[280px]"> | ||
| <Image | ||
| src={imageUrl} | ||
| alt={name} | ||
| width={width} | ||
| height={height} | ||
| fit="cover" | ||
| loading="lazy" | ||
| fetchPriority="low" | ||
| class="w-full h-full object-cover block" | ||
| /> | ||
| {badge && ( | ||
| <span class="absolute top-4 left-4 text-xs font-semibold px-2.5 py-1 rounded-full bg-accent text-inverted leading-none"> | ||
| {badge} | ||
| </span> | ||
| )} | ||
| </div> | ||
| <div class="p-7 flex flex-col gap-3 justify-center sm:w-3/5"> | ||
| <h3 class="text-xl font-bold leading-snug m-0">{name}</h3> | ||
| {productDescription && ( | ||
| <div | ||
| class="text-[0.9375rem] text-secondary leading-relaxed [text-wrap:pretty]" | ||
| dangerouslySetInnerHTML={{ | ||
| __html: sanitizeHtml(productDescription), | ||
| }} | ||
| /> | ||
| )} | ||
| <div class="flex items-baseline gap-2.5 mt-1"> | ||
| {price && ( | ||
| <span class="text-2xl font-bold text-accent tabular-nums"> | ||
| {price} | ||
| </span> | ||
| )} | ||
| {listPrice && listPrice !== price && ( | ||
| <span class="text-base text-muted line-through tabular-nums"> | ||
| {listPrice} | ||
| </span> | ||
| )} | ||
| </div> | ||
| {safeUrl && ( | ||
| <a | ||
| href={safeUrl} | ||
| {...(isExternal | ||
| ? { target: "_blank", rel: "noopener noreferrer" } | ||
| : {})} | ||
| class="mt-2 self-start inline-flex items-center justify-center py-[0.7em] px-[2em] rounded-full bg-accent text-inverted font-semibold text-sm tracking-[0.02em] no-underline transition-opacity hover:opacity-85" | ||
| > | ||
| {cta ?? "Comprar"} | ||
| </a> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function LoadingFallback() { | ||
| return ( | ||
| <div class="not-prose my-10 border border-line rounded-brand bg-base animate-pulse"> | ||
| <div class="flex flex-col sm:flex-row"> | ||
| <div class="sm:w-2/5 aspect-video sm:aspect-auto bg-alt" /> | ||
| <div class="p-7 sm:w-3/5 flex flex-col gap-3"> | ||
| <div class="h-7 bg-alt rounded w-3/4" /> | ||
| <div class="h-4 bg-alt rounded" /> | ||
| <div class="h-4 bg-alt rounded w-5/6" /> | ||
| <div class="h-8 bg-alt rounded w-1/3 mt-2" /> | ||
| <div class="h-10 bg-alt rounded-full w-40 mt-2" /> | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.