diff --git a/blog/loaders/BlogRelatedPosts.ts b/blog/loaders/BlogRelatedPosts.ts new file mode 100644 index 000000000..5e74632cd --- /dev/null +++ b/blog/loaders/BlogRelatedPosts.ts @@ -0,0 +1,96 @@ +/** + * @title BlogRelatedPosts + * @description Retrieves a list of blog related posts. + * + * @param props - The props for the blog related post list. + * @param req - The request object. + * @param ctx - The application context. + * @returns A promise that resolves to an array of blog related posts. + */ +import { RequestURLParam } from "../../website/functions/requestToParam.ts"; +import { AppContext } from "../mod.ts"; +import { BlogPost, SortBy } from "../types.ts"; +import handlePosts, { slicePosts } from "../utils/handlePosts.ts"; +import { getRecordsByPath } from "../utils/records.ts"; + +const COLLECTION_PATH = "collections/blog/posts"; +const ACCESSOR = "post"; + +export interface Props { + /** + * @title Items per page + * @description Number of posts per page to display. + */ + count?: number; + /** + * @title Page query parameter + * @description The current page number. Defaults to 1. + */ + page?: number; + /** + * @title Category Slug + * @description Filter by a specific category slug. + */ + slug?: RequestURLParam | string[]; + /** + * @title Page sorting parameter + * @description The sorting option. Default is "date_desc" + */ + sortBy?: SortBy; + /** + * @description Overrides the query term at url + */ + query?: string; + /** + * @title Exclude Post Slug + * @description Excludes a post slug from the list + */ + excludePostSlug?: RequestURLParam | string; +} + +/** + * @title BlogRelatedPosts + * @description Retrieves a list of blog related posts. + * + * @param props - The props for the blog related post list. + * @param req - The request object. + * @param ctx - The application context. + * @returns A promise that resolves to an array of blog related posts. + */ + +export type BlogRelatedPosts = BlogPost[] | null; + +export default async function BlogRelatedPosts( + { page, count, slug, sortBy, query, excludePostSlug }: Props, + req: Request, + ctx: AppContext, +): Promise { + const url = new URL(req.url); + const postsPerPage = Number(count ?? url.searchParams.get("count") ?? 12); + const pageNumber = Number(page ?? url.searchParams.get("page") ?? 1); + const pageSort = sortBy ?? (url.searchParams.get("sortBy") as SortBy) ?? + "date_desc"; + const term = query ?? url.searchParams.get("q") ?? undefined; + + const posts = await getRecordsByPath( + ctx, + COLLECTION_PATH, + ACCESSOR, + ); + + const handledPosts = handlePosts( + posts, + pageSort, + slug, + term, + excludePostSlug, + ); + + if (!handledPosts) { + return null; + } + + const slicedPosts = slicePosts(handledPosts, pageNumber, postsPerPage); + + return slicedPosts.length > 0 ? slicedPosts : null; +} diff --git a/blog/loaders/BlogpostList.ts b/blog/loaders/BlogpostList.ts index f0d8dcfc0..3c3e1f996 100644 --- a/blog/loaders/BlogpostList.ts +++ b/blog/loaders/BlogpostList.ts @@ -57,7 +57,7 @@ export default async function BlogPostList( ctx: AppContext, ): Promise { const url = new URL(req.url); - const postsPerPage = Number(count ?? url.searchParams.get("count")); + const postsPerPage = Number(count ?? url.searchParams.get("count") ?? 12); const pageNumber = Number(page ?? url.searchParams.get("page") ?? 1); const pageSort = sortBy ?? url.searchParams.get("sortBy") as SortBy ?? "date_desc"; diff --git a/blog/manifest.gen.ts b/blog/manifest.gen.ts index 157fa04d7..9709a8117 100644 --- a/blog/manifest.gen.ts +++ b/blog/manifest.gen.ts @@ -10,6 +10,7 @@ import * as $$$5 from "./loaders/BlogpostListing.ts"; import * as $$$2 from "./loaders/BlogPostPage.ts"; import * as $$$6 from "./loaders/Category.ts"; import * as $$$7 from "./loaders/GetCategories.ts"; +import * as $$$8 from "./loaders/BlogRelatedPosts.ts"; import * as $$$$$$0 from "./sections/Seo/SeoBlogPost.tsx"; import * as $$$$$$1 from "./sections/Seo/SeoBlogPostListing.tsx"; import * as $$$$$$2 from "./sections/Template.tsx"; @@ -24,6 +25,7 @@ const manifest = { "blog/loaders/BlogPostPage.ts": $$$2, "blog/loaders/Category.ts": $$$6, "blog/loaders/GetCategories.ts": $$$7, + "blog/loaders/BlogRelatedPosts.ts": $$$8, }, "sections": { "blog/sections/Seo/SeoBlogPost.tsx": $$$$$$0, diff --git a/blog/types.ts b/blog/types.ts index 222b953e0..0450ce82a 100644 --- a/blog/types.ts +++ b/blog/types.ts @@ -58,6 +58,8 @@ export interface BlogPost { * @title Extra Props */ extraProps?: ExtraProps[]; + /** @hide true */ + id?: string; } export interface ExtraProps { diff --git a/blog/utils/handlePosts.ts b/blog/utils/handlePosts.ts index 538d2f3e0..99f398fa5 100644 --- a/blog/utils/handlePosts.ts +++ b/blog/utils/handlePosts.ts @@ -58,6 +58,20 @@ export const filterPostsByTerm = (posts: BlogPost[], term: string) => ) ); +/** + * Returns an filtered BlogPost list + * + * @param posts Posts to be handled + * @param slug Category Slug to be filter + */ +export const filterRelatedPosts = ( + posts: BlogPost[], + slug: string[], +) => + posts.filter( + ({ categories }) => categories.find((c) => slug.includes(c.slug)), + ); + /** * Returns an filtered and sorted BlogPost list * @@ -77,11 +91,15 @@ export const slicePosts = ( export const filterPosts = ( posts: BlogPost[], - slug?: string, + slug?: string | string[], term?: string, ): BlogPost[] => { if (term) return filterPostsByTerm(posts, term); - if (slug) return filterPostsByCategory(posts, slug); + if (typeof slug === "string") return filterPostsByCategory(posts, slug); + if (Array.isArray(slug)) { + return filterRelatedPosts(posts, slug); + } + return posts; }; @@ -90,16 +108,20 @@ export const filterPosts = ( * * @param posts Posts to be handled * @param sortBy Sort option (must be: "date_desc" | "date_asc" | "title_asc" | "title_desc" ) - * @param slug Category slug to be filter + * @param slug Category slug or an array of slugs to be filtered * @param term Term to be filter + * @param excludePostSlug Post slug to be excluded */ export default function handlePosts( posts: BlogPost[], sortBy: SortBy, - slug?: string, + slug?: string | string[], term?: string, + excludePostSlug?: string, ) { - const filteredPosts = filterPosts(posts, slug, term); + const filteredPosts = filterPosts(posts, slug, term).filter( + ({ slug: postSlug }) => postSlug !== excludePostSlug, + ); if (!filteredPosts || filteredPosts.length === 0) { return null; diff --git a/blog/utils/records.ts b/blog/utils/records.ts index 6e19e463b..e65890417 100644 --- a/blog/utils/records.ts +++ b/blog/utils/records.ts @@ -11,5 +11,11 @@ export async function getRecordsByPath( const current = Object.entries(resolvables).flatMap(([key, value]) => { return key.startsWith(path) ? value : []; }); - return (current as Record[]).map((item) => item[accessor]); + return (current as Record[]).map((item) => { + const id = (item.name as string).split(path)[1]?.replace("/", ""); + return { + ...item[accessor], + id, + }; + }); } diff --git a/deno.json b/deno.json index 798c215f7..4024c21f7 100644 --- a/deno.json +++ b/deno.json @@ -68,5 +68,5 @@ "jsx": "react-jsx", "jsxImportSource": "preact" }, - "version": "0.64.11" + "version": "0.64.15" } diff --git a/vtex/utils/transform.ts b/vtex/utils/transform.ts index 4010133f3..2b155d971 100644 --- a/vtex/utils/transform.ts +++ b/vtex/utils/transform.ts @@ -32,6 +32,7 @@ import type { LegacyProduct as LegacyProductVTEX, OrderForm, PageType as PageTypeVTEX, + PickupHolidays, PickupPoint, Product as ProductVTEX, ProductInventoryData, @@ -1120,6 +1121,23 @@ function toHoursSpecification(hours: Hours): OpeningHoursSpecification { }; } +function toSpecialHoursSpecification( + holiday: PickupHolidays, +): OpeningHoursSpecification { + const dateHoliday = new Date(holiday.date ?? ""); + // VTEX provide date in ISO format, at 00h on the day + const validThrough = dateHoliday.setDate(dateHoliday.getDate() + 1) + .toString(); + + return { + "@type": "OpeningHoursSpecification", + opens: holiday.hourBegin, + closes: holiday.hourEnd, + validFrom: holiday.date, + validThrough, + }; +} + function isPickupPointVCS( pickupPoint: PickupPoint | PickupPointVCS, ): pickupPoint is PickupPointVCS { @@ -1135,12 +1153,16 @@ export function toPlace( latitude, longitude, openingHoursSpecification, + specialOpeningHoursSpecification, } = isPickupPointVCS(pickupPoint) ? { name: pickupPoint.name, country: pickupPoint.address?.country?.acronym, latitude: pickupPoint.address?.location?.latitude, longitude: pickupPoint.address?.location?.longitude, + specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map( + toSpecialHoursSpecification, + ), openingHoursSpecification: pickupPoint.businessHours?.map( toHoursSpecification, ), @@ -1150,6 +1172,9 @@ export function toPlace( country: pickupPoint.address?.country, latitude: pickupPoint.address?.geoCoordinates[0], longitude: pickupPoint.address?.geoCoordinates[1], + specialOpeningHoursSpecification: pickupPoint.pickupHolidays?.map( + toSpecialHoursSpecification, + ), openingHoursSpecification: pickupPoint.businessHours?.map(( { ClosingTime, DayOfWeek, OpeningTime }, ) => @@ -1175,6 +1200,7 @@ export function toPlace( latitude, longitude, name, + specialOpeningHoursSpecification, openingHoursSpecification, additionalProperty: [{ "@type": "PropertyValue", diff --git a/vtex/utils/types.ts b/vtex/utils/types.ts index 0aafb0740..50ec9a8a5 100644 --- a/vtex/utils/types.ts +++ b/vtex/utils/types.ts @@ -449,12 +449,19 @@ export interface PickupStoreInfo { dockId: null; } +export interface PickupHolidays { + date?: string; + hourBegin?: string; + hourEnd?: string; +} + export interface PickupPoint { friendlyName: string; address: Address; additionalInfo: string; id: string; businessHours: BusinessHour[]; + pickupHolidays?: PickupHolidays[]; } export interface BusinessHour { diff --git a/website/components/OneDollarStats.tsx b/website/components/OneDollarStats.tsx new file mode 100644 index 000000000..a4b1385d8 --- /dev/null +++ b/website/components/OneDollarStats.tsx @@ -0,0 +1,92 @@ +import { Head } from "$fresh/runtime.ts"; +import { useScriptAsDataURI } from "@deco/deco/hooks"; + +export interface Props { + /** + * @description collector address to use + */ + collectorAddress?: string; +} + +declare global { + interface Window { + trackCustomEvent: ( + name: string, + params: Record, + ) => void; + } +} + +// we are forcing domain to be deco.cx +// because the domain is separated from the 'u' property +const trackerOriginal = + `"use strict";(()=>{var L;var V=-1,m=function(e){addEventListener("pageshow",function(t){t.persisted&&(V=t.timeStamp,e(t))},!0)},_=function(){var e=self.performance&&performance.getEntriesByType&&performance.getEntriesByType("navigation")[0];if(e&&e.responseStart>0&&e.responseStart=0?r="back-forward-cache":n&&(document.prerendering||I()>0?r="prerender":document.wasDiscarded?r="restore":n.type&&(r=n.type.replace(/_/g,"-"))),{name:e,value:t===void 0?-1:t,rating:"good",delta:0,entries:[],id:"v4-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12),navigationType:r}},g=function(e,t,n){try{if(PerformanceObserver.supportedEntryTypes.includes(e)){var r=new PerformanceObserver(function(i){Promise.resolve().then(function(){t(i.getEntries())})});return r.observe(Object.assign({type:e,buffered:!0},n||{})),r}}catch{}},v=function(e,t,n,r){var i,o;return function(u){t.value>=0&&(u||r)&&((o=t.value-(i||0))||i===void 0)&&(i=t.value,t.delta=o,t.rating=function(c,a){return c>a[1]?"poor":c>a[0]?"needs-improvement":"good"}(t.value,n),e(t))}},A=function(e){requestAnimationFrame(function(){return requestAnimationFrame(function(){return e()})})},b=function(e){document.addEventListener("visibilitychange",function(){document.visibilityState==="hidden"&&e()})},R=function(e){var t=!1;return function(){t||(e(),t=!0)}},h=-1,B=function(){return document.visibilityState!=="hidden"||document.prerendering?1/0:0},E=function(e){document.visibilityState==="hidden"&&h>-1&&(h=e.type==="visibilitychange"?e.timeStamp:0,ne())},M=function(){addEventListener("visibilitychange",E,!0),addEventListener("prerenderingchange",E,!0)},ne=function(){removeEventListener("visibilitychange",E,!0),removeEventListener("prerenderingchange",E,!0)},D=function(){return h<0&&(h=B(),M(),m(function(){setTimeout(function(){h=B(),M()},0)})),{get firstHiddenTime(){return h}}},k=function(e){document.prerendering?addEventListener("prerenderingchange",function(){return e()},!0):e()},N=[1800,3e3],ie=function(e,t){t=t||{},k(function(){var n,r=D(),i=p("FCP"),o=g("paint",function(u){u.forEach(function(c){c.name==="first-contentful-paint"&&(o.disconnect(),c.startTimer.value&&(r.value=i,r.entries=o,n())},c=g("layout-shift",u);c&&(n=v(e,r,j,t.reportAllChanges),b(function(){u(c.takeRecords()),n(!0)}),m(function(){i=0,r=p("CLS",0),n=v(e,r,j,t.reportAllChanges),A(function(){return n()})}),setTimeout(n,0))}))},$=0,S=1/0,y=0,re=function(e){e.forEach(function(t){t.interactionId&&(S=Math.min(S,t.interactionId),y=Math.max(y,t.interactionId),$=y?(y-S)/7+1:0)})},J=function(){return L?$:performance.interactionCount||0},oe=function(){"interactionCount"in performance||L||(L=g("event",re,{type:"event",buffered:!0,durationThreshold:0}))},f=[],T=new Map,Q=0,ae=function(){var e=Math.min(f.length-1,Math.floor((J()-Q)/50));return f[e]},ue=[],ce=function(e){if(ue.forEach(function(i){return i(e)}),e.interactionId||e.entryType==="first-input"){var t=f[f.length-1],n=T.get(e.interactionId);if(n||f.length<10||e.duration>t.latency){if(n)e.duration>n.latency?(n.entries=[e],n.latency=e.duration):e.duration===n.latency&&e.startTime===n.entries[0].startTime&&n.entries.push(e);else{var r={id:e.interactionId,latency:e.duration,entries:[e]};T.set(r.id,r),f.push(r)}f.sort(function(i,o){return o.latency-i.latency}),f.length>10&&f.splice(10).forEach(function(i){return T.delete(i.id)})}}},z=function(e){var t=self.requestIdleCallback||self.setTimeout,n=-1;return e=R(e),document.visibilityState==="hidden"?e():(n=t(e),b(e)),n},x=[200,500],G=function(e,t){"PerformanceEventTiming"in self&&"interactionId"in PerformanceEventTiming.prototype&&(t=t||{},k(function(){var n;oe();var r,i=p("INP"),o=function(c){z(function(){c.forEach(ce);var a=ae();a&&a.latency!==i.value&&(i.value=a.latency,i.entries=a.entries,r())})},u=g("event",o,{durationThreshold:(n=t.durationThreshold)!==null&&n!==void 0?n:40});r=v(e,i,x,t.reportAllChanges),u&&(u.observe({type:"first-input",buffered:!0}),b(function(){o(u.takeRecords()),r(!0)}),m(function(){Q=J(),f.length=0,T.clear(),i=p("INP"),r=v(e,i,x,t.reportAllChanges)}))}))},H=[2500,4e3],P={},K=function(e,t){t=t||{},k(function(){var n,r=D(),i=p("LCP"),o=function(a){t.reportAllChanges||(a=a.slice(-1)),a.forEach(function(d){d.startTimeconsole.error('fetch() failed: ' + n.message))}function s(e,t){var n;if(!((n=window.unexpected)===null||n===void 0)&&n.q||(window.unexpected={q:[]}),window.unexpected.q.push(e),t===0){s.timeout&&(clearTimeout(s.timeout.id),s.timeout=null),C();return}let r=()=>{C(),s.timeout=null};if(!s.timeout){s.timeout={id:setTimeout(r,t),delay:t};return}s.timeout.delay>=t&&(clearTimeout(s.timeout.id),s.timeout={id:setTimeout(r,t),delay:t})}(function(e){e.timeout=null})(s||(s={}));function C(){var e;if(!(!((e=window.unexpected)===null||e===void 0)&&e.q)||!window.unexpected.q.length)return;s.timeout!==null&&clearTimeout(s.timeout.id);let t=window.unexpected.q;window.unexpected.q=[];let n={u:t[0].u,v:t[0].v,e:[]};for(let i of t)switch(i.t){case"CwvReport":n.e.push({t:"CwvReport",cls:i.cls,inp:i.inp,lcp:i.lcp});break;case"PageView":n.e.push({t:"PageView",h:i.h,r:i.r});break}let r=Z();Y(r,n)}async function q(){let e=new URL(location.href);return e.search="", + + {d:"deco.cx", u:e.href}} + + async function O(e){if(W())return w("CwvReport","Running in a headless browser");if(X())return w("CwvReport","Ignore flag is set");s(Object.assign(Object.assign({},await q()),{t:"CwvReport",cls:e.name==="CLS"?e.value:void 0,inp:e.name==="INP"?e.value:void 0,lcp:e.name==="LCP"?e.value:void 0}),5e3)}async function l(){if(W())return w("PageView","Running in a headless browser");if(X())return w("PageView","Ignore flag is set");if(!F&&l.lastPage===location.pathname)return w("PageView","Pathname has not changed");l.lastPage=location.pathname;let e=new URL(location.href),t=document.referrer?new URL(document.referrer):void 0;t&&(t.search=""),s(Object.assign(Object.assign({},await q()),{t:"PageView",h:F,r:t&&t.hostname!==e.hostname?t.href:void 0}),0)}(function(e){e.lastPage=null})(l||(l={}));var de=document.currentScript,F=se(de);U(O);G(O);K(O);if(window.history.pushState){let e=window.history.pushState;window.history.pushState=function(t,n,r){e.apply(this,[t,n,r]),l()},window.addEventListener("popstate",l)}document.visibilityState!=="visible"?document.addEventListener("visibilitychange",()=>{!l.lastPage&&document.visibilityState==="visible"&&l()}):l();document.addEventListener("visibilitychange",()=>{document.visibilityState});document.addEventListener("pagehide",C);async function fe(e,t){let n=Object.assign(Object.assign({},await q()),{e:[{t:e,p:t||void 0,h:F}]}),r=Z();Y(r,n)}function Z(){let e=document.querySelector("#tracker")||document.currentScript,t=e?.getAttribute("data-url");if(!t)throw new Error("No url provided to data-url attribute");return t}window.trackCustomEvent=fe;})();`; + +const snippet = () => { + // Flags and additional dimentions + const props: Record = {}; + const trackPageview = () => + globalThis.window.trackCustomEvent?.("pageview", props); + // Attach pushState and popState listeners + const originalPushState = history.pushState; + if (originalPushState) { + history.pushState = function () { + // @ts-ignore monkey patch + originalPushState.apply(this, arguments); + trackPageview(); + }; + addEventListener("popstate", trackPageview); + } + // 2000 bytes limit + const truncate = (str: string) => `${str}`.slice(0, 990); + + globalThis.window.DECO.events.subscribe((event) => { + if (!event) { + return; + } + const { name, params } = event; + if (!name || !params || name === "deco") { + return; + } + const values = { ...props }; + for (const key in params) { + // @ts-expect-error somehow typescript bugs + const value = params[key]; + if (value !== null && value !== undefined) { + values[key] = truncate( + typeof value !== "object" ? value : JSON.stringify(value), + ); + } + } + globalThis.window.trackCustomEvent?.(name, values); + }); +}; + +function Component({ collectorAddress }: Props) { + const collector = collectorAddress ?? "https://collector.deco.cx/events"; + const tracker = trackerOriginal.replace("COLLECTOR_ADDRESS", collector); + + return ( + + + +