diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d3211da --- /dev/null +++ b/.env.example @@ -0,0 +1,10 @@ +VITE_GRAPHQL_ENDPOINT=https://admin.e-multicontent.com/graphql +VITE_ARTIFACT_ROUTE_HYDRATION=true +VITE_BACKEND_PROFILE=full +VITE_SITE_URL=https://e-multicontent.com +STOREFRONT_ARTIFACT_SITE_KEY=e-multi +STOREFRONT_ARTIFACT_MODE=off +CMS_TAILWIND_SOURCE_API_URL=https://admin.e-multicontent.com/wp-json/funkycommerce-tailwind/v1 +CMS_TAILWIND_BOOTSTRAP=true +CMS_TAILWIND_REQUIRED=true +STOREFRONT_ARTIFACT_SIGNING_SECRET=e200a0a19a618dd149af13f7f6a83124a0b57e50251506692a1e07d908f04a54 \ No newline at end of file diff --git a/apps/storefront/.env.example b/apps/storefront/.env.example deleted file mode 100644 index 4702126..0000000 --- a/apps/storefront/.env.example +++ /dev/null @@ -1,34 +0,0 @@ -# Single source of truth for the backend connection. Every other backend-derived -# value (auth mutations, abandoned-cart tracking endpoint, live shop currency, payment -# gateway availability) is derived from this one URL's origin — see lib/env.ts. Point -# it at your WPGraphQL endpoint, e.g.: -VITE_GRAPHQL_ENDPOINT= -# -# Optional direct URL to the static Tailwind class manifest generated by the -# Optional static Tailwind manifest for deployments that publish one independently. -# Superfunky production leaves this empty and compiles only the reviewed local -# contract, performing no CMS Tailwind inventory request. A configured manifest -# fails before Vite when unavailable, incomplete, or invalid. -VITE_CMS_TAILWIND_MANIFEST_URL= -# -# Preferred incremental mode: a signed, read-only source endpoint. Requires -# STOREFRONT_ARTIFACT_SIGNING_SECRET and VITE_SITE_URL. The build downloads the -# previous deployed index, fetches only changed source bodies, and publishes the -# next index with the successful deployment. -CMS_TAILWIND_SOURCE_API_URL= -# Set only for the first deployment when no previous index exists. A successful -# deployment publishes the index; remove this flag afterward. -CMS_TAILWIND_BOOTSTRAP=false -# -# Development and production builds default to the public -# https://dev.superfunky.pro/graphql reference backend when this is empty. Set an -# explicit endpoint for every customer or production deployment. -# -# Web Push subscribe, unsubscribe, and VAPID public-key routes are derived from this -# backend origin. The Stripe publishable key is likewise supplied by backend store -# settings. Private VAPID and Stripe keys must never be exposed to the frontend. - -# Edge/CDN geolocation endpoint (see lib/geolocation.ts) — typically injected by the -# hosting platform (e.g. Netlify's /geolocation) rather than the WP backend itself, so -# it stays a separate env var. -VITE_GEOLOCATION_ENDPOINT= diff --git a/apps/storefront/src/components/CmsPageContent.tsx b/apps/storefront/src/components/CmsPageContent.tsx index 1aba92c..07d46ea 100644 --- a/apps/storefront/src/components/CmsPageContent.tsx +++ b/apps/storefront/src/components/CmsPageContent.tsx @@ -245,47 +245,47 @@ export function CmsPageContent({ ? `${window.location.origin}${pathname === "/" ? "" : pathname.replace(/\/+$/, "")}` : page.seo.canonical || page.seo.opengraphUrl || `${window.location.origin}${pathname}`} languageCode={contentLanguageCode} - keywords={page.seo.keywords || undefined} - siteName={page.seo.siteName || undefined} - appendSiteName={false} - robots={ - robots - || ( - homePath - ? "index, follow" - : page.seo.robotsSource === "explicit" - ? page.seo.robots - : fallbackRobots || page.seo.robots - ) + keywords={page.seo.keywords || undefined} + siteName={page.seo.siteName || undefined} + appendSiteName={false} + robots={ + robots + || ( + homePath + ? "index, follow" + : page.seo.robotsSource === "explicit" + ? page.seo.robots + : fallbackRobots || page.seo.robots + ) + } + opengraphType={page.seo.opengraphType === "article" ? "article" : "website"} + opengraphTitle={page.seo.opengraphTitle || undefined} + opengraphDescription={page.seo.opengraphDescription || undefined} + image={page.featuredImage + ? { + url: page.featuredImage.sourceUrl, + alt: page.featuredImage.altText || page.title, + width: page.featuredImage.width, + height: page.featuredImage.height, } - opengraphType={page.seo.opengraphType === "article" ? "article" : "website"} - opengraphTitle={page.seo.opengraphTitle || undefined} - opengraphDescription={page.seo.opengraphDescription || undefined} - image={page.featuredImage - ? { - url: page.featuredImage.sourceUrl, - alt: page.featuredImage.altText || page.title, - width: page.featuredImage.width, - height: page.featuredImage.height, - } - : page.seo.opengraphImage - ? { url: page.seo.opengraphImage, alt: page.title } - : undefined} - opengraphPublishedTime={page.seo.opengraphPublishedTime || undefined} - opengraphModifiedTime={page.seo.opengraphModifiedTime || undefined} - opengraphAuthor={page.seo.opengraphAuthor || undefined} - opengraphPublisher={page.seo.opengraphPublisher || undefined} - twitterTitle={page.seo.twitterTitle || undefined} - twitterDescription={page.seo.twitterDescription || undefined} - schema={{ - pageType: page.seo.pageType || undefined, - articleType: page.seo.articleType || undefined, - }} - breadcrumbs={page.seo.breadcrumbs} - translations={page.translations.map((translation) => ({ - languageCode: translation.languageCode, - url: translation.uri, - }))} + : page.seo.opengraphImage + ? { url: page.seo.opengraphImage, alt: page.title } + : undefined} + opengraphPublishedTime={page.seo.opengraphPublishedTime || undefined} + opengraphModifiedTime={page.seo.opengraphModifiedTime || undefined} + opengraphAuthor={page.seo.opengraphAuthor || undefined} + opengraphPublisher={page.seo.opengraphPublisher || undefined} + twitterTitle={page.seo.twitterTitle || undefined} + twitterDescription={page.seo.twitterDescription || undefined} + schema={{ + pageType: page.seo.pageType || undefined, + articleType: page.seo.articleType || undefined, + }} + breadcrumbs={page.seo.breadcrumbs} + translations={page.translations.map((translation) => ({ + languageCode: translation.languageCode, + url: translation.uri, + }))} /> {breadcrumbs.length > 1 ? : null} @@ -372,7 +372,8 @@ function NestedWordPressShortcodes({ html, slots }: { html: string; slots: Neste return ( <> -
+ +
{slots.map(({ marker, content }, index) => ( -
-
- Loading {marker.name} -
+
+
+
+ Loading {marker.name}
- ), +
+ ), target, marker.slotId, ); diff --git a/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx b/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx new file mode 100644 index 0000000..063caa8 --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsExistingMarkupAssets.tsx @@ -0,0 +1,219 @@ + +import { useEffect } from "react"; +import { graphqlRequest } from "@funky/sdk"; +import "./ElevenLabsHeadlessReader.css"; + +type AssetsResponse = { + elwrHeadlessAssets: { + css: string | null; + scriptUrl: string | null; + styleUrl: string | null; + } | null; +}; + +type ReaderResponse = { + post: { + elwrReaderHtml: string | null; + } | null; +}; + +const ASSETS_QUERY = ` + query ElevenLabsAssets { + elwrHeadlessAssets { + css + scriptUrl + styleUrl + } + } +`; + +const READER_QUERY = ` + query ElevenLabsReaderColor($id: ID!) { + post(id: $id, idType: DATABASE_ID) { + elwrReaderHtml + } + } +`; + +type Props = { + postId?: number; +}; + +function extractReaderColor(html: string): string | null { + const doc = new DOMParser().parseFromString(html, "text/html"); + + const reader = doc.querySelector(".elwr-reader"); + if (!reader) return null; + + const color = reader.style + .getPropertyValue("--elwr-color") + .trim(); + + // Accept only hex colors from the backend. + return /^#[0-9a-fA-F]{3,8}$/.test(color) ? color : null; +} + +function applyReaderColor(color: string) { + document.querySelectorAll(".elwr-reader").forEach( + (reader) => { + reader.style.setProperty("--elwr-color", color); + } + ); +} + +export function ElevenLabsExistingMarkupAssets({ + postId, +}: Props) { + useEffect(() => { + let cancelled = false; + let observer: MutationObserver | null = null; + + async function loadAssets() { + try { + const { data, errors } = + await graphqlRequest(ASSETS_QUERY); + + if (errors?.length) { + throw new Error( + errors.map((error) => error.message).join("; ") + ); + } + + if (cancelled || !data?.elwrHeadlessAssets) { + return; + } + + const { css, scriptUrl, styleUrl } = + data.elwrHeadlessAssets; + + // 1. Load the plugin's original CSS. + if (styleUrl) { + let link = document.getElementById( + "elevenlabs-reader-stylesheet" + ) as HTMLLinkElement | null; + + if (!link) { + link = document.createElement("link"); + link.id = "elevenlabs-reader-stylesheet"; + link.rel = "stylesheet"; + document.head.appendChild(link); + } + + if (link.href !== styleUrl) { + link.href = styleUrl; + } + } + + // 2. Inject WordPress dynamic CSS. + if (css) { + let style = document.getElementById( + "elevenlabs-dynamic-css" + ) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = "elevenlabs-dynamic-css"; + document.head.appendChild(style); + } + + style.textContent = css; + } + + // 3. Restore the WordPress admin color if + // the frontend sanitizer removed it. + if (Number.isInteger(postId) && (postId ?? 0) > 0) { + try { + const readerResult = + await graphqlRequest( + READER_QUERY, + { id: String(postId) } + ); + + if (readerResult.errors?.length) { + throw new Error( + readerResult.errors + .map((error) => error.message) + .join("; ") + ); + } + + if (cancelled) return; + + const html = + readerResult.data?.post?.elwrReaderHtml; + + const color = html + ? extractReaderColor(html) + : null; + + if (color) { + applyReaderColor(color); + + // Also handle reader markup mounted later. + observer = new MutationObserver(() => { + applyReaderColor(color); + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + + console.info( + "[ELWR] WordPress admin color:", + color + ); + } else { + console.warn( + "[ELWR] No admin color found in reader HTML" + ); + } + } catch (error) { + console.warn( + "[ELWR] Could not restore reader color:", + error + ); + } + } + + if (cancelled) return; + + // 4. Load the plugin's JavaScript. + if (scriptUrl) { + let script = document.getElementById( + "elevenlabs-reader-script" + ) as HTMLScriptElement | null; + + if (!script) { + script = document.createElement("script"); + script.id = "elevenlabs-reader-script"; + script.async = true; + script.src = scriptUrl; + document.body.appendChild(script); + } + } + + console.info("[ELWR] Assets loaded", { + css: Boolean(css), + styleUrl, + scriptUrl, + postId, + }); + } catch (error) { + console.error( + "[ELWR] Failed to load assets:", + error + ); + } + } + + loadAssets(); + + return () => { + cancelled = true; + observer?.disconnect(); + }; + }, [postId]); + + return null; +} diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.css b/apps/storefront/src/components/ElevenLabsHeadlessReader.css new file mode 100644 index 0000000..e1c769d --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.css @@ -0,0 +1,16 @@ +.elwr-headless-root { + width: 100%; + max-width: 100%; +} + +.elwr-headless-root .elwr-reader { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +.elwr-headless-root .elwr-reader *, +.elwr-headless-root .elwr-reader *::before, +.elwr-headless-root .elwr-reader *::after { + box-sizing: border-box; +} \ No newline at end of file diff --git a/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx new file mode 100644 index 0000000..e0c6909 --- /dev/null +++ b/apps/storefront/src/components/ElevenLabsHeadlessReader.tsx @@ -0,0 +1,108 @@ + +import "./ElevenLabsHeadlessReader.css"; +import { fetchElevenLabsReader } from "../lib/elevenlabs"; +import { useEffect, useState } from "react"; + +type Props = { + postId: number; +}; + +export function ElevenLabsHeadlessReader({ postId }: Props) { + const [html, setHtml] = useState(""); + const [css, setCss] = useState(""); + const [scriptUrl, setScriptUrl] = useState(""); + const [error, setError] = useState(null); + + useEffect(() => { + let active = true; + + setHtml(""); + setError(null); + + fetchElevenLabsReader(postId) + .then((result) => { + if (!active) return; + + setHtml(result.post?.elwrReaderHtml ?? ""); + setCss(result.elwrHeadlessAssets?.css ?? ""); + setScriptUrl(result.elwrHeadlessAssets?.scriptUrl ?? ""); + console.log('asdasd', result.post?.elwrReaderHtml ?? "") + console.log('asdasd', result.elwrHeadlessAssets?.css ?? "") + console.log(result.elwrHeadlessAssets?.scriptUrl ?? "") + }) + .catch((reason: unknown) => { + if (!active) return; + + setError( + reason instanceof Error + ? reason.message + : "ElevenLabs reader could not be loaded" + ); + }); + + return () => { + active = false; + }; + }, [postId]); + + // Load WordPress plugin CSS + useEffect(() => { + if (!css) return; + + const id = "elevenlabs-dynamic-css"; + + let style = document.getElementById(id) as HTMLStyleElement | null; + + if (!style) { + style = document.createElement("style"); + style.id = id; + document.head.appendChild(style); + } + + style.textContent = css; + }, [css]); + + // Load WordPress plugin JavaScript + useEffect(() => { + if (!scriptUrl) return; + + const id = "elevenlabs-reader-script"; + + let script = document.getElementById(id) as HTMLScriptElement | null; + + if (script?.src === scriptUrl) return; + + if (script) { + script.remove(); + } + + script = document.createElement("script"); + script.id = id; + script.src = scriptUrl; + script.async = true; + + script.onerror = () => { + setError("ElevenLabs player JavaScript failed to load."); + }; + + document.body.appendChild(script); + + return () => { + // Keep the shared script loaded; its click handler uses event delegation. + }; + }, [scriptUrl]); + + if (error) { + return

{error}

; + } + + if (!html) return null; + + return ( +
+ ); +} diff --git a/apps/storefront/src/components/wordpressShortcodes.tsx b/apps/storefront/src/components/wordpressShortcodes.tsx index 7305429..61fcc26 100644 --- a/apps/storefront/src/components/wordpressShortcodes.tsx +++ b/apps/storefront/src/components/wordpressShortcodes.tsx @@ -58,6 +58,7 @@ import { communityMemberMatchesRoles, normalizeCommunityMemberTypes, } from "../lib/communityMemberTypes"; +import { ElevenLabsHeadlessReader } from "./ElevenLabsHeadlessReader"; export type WordPressShortcodeAttributes = Record; export type WordPressShortcodeRenderer = (attributes: WordPressShortcodeAttributes) => ReactNode; @@ -284,47 +285,46 @@ function SliderShortcode({ attributes }: ShortcodeProps) { renderItem={(slide, index, meta) => { const SlideHeading = index === 0 ? firstSlideHeadingLevel : slideHeadingLevel; return ( -
- {slide.image ? : null} -
+ {slide.image ? : null} + @@ -412,11 +420,10 @@ function TableOfContents({ entries }: { entries: TocEntry[] }) { {entry.text} @@ -441,24 +448,21 @@ function TocDotRail({ entries, side }: { entries: TocEntry[]; side: "left" | "ri return (