diff --git a/app/components/HomeHeader/HomeHeader.container.tsx b/app/components/HomeHeader/HomeHeader.container.tsx index ee15699..ad5df51 100644 --- a/app/components/HomeHeader/HomeHeader.container.tsx +++ b/app/components/HomeHeader/HomeHeader.container.tsx @@ -1,71 +1,180 @@ "use client"; -import { memo, useMemo } from "react"; +import { memo } from "react"; import { usePathname } from "next/navigation"; -import { useSchemaData } from "../../hooks"; +import MenuBarItem from "../MenuBarItem"; +import Button from "../Button"; +import AvatarContainer from "../AvatarContainer"; +import Avatar from "../Avatar"; +import Logo from "../Logo"; +import { getAssetPath, ASSETS } from "../../../lib/assetUtils"; import HomeHeaderView from "./HomeHeader.view"; -import type { HomeHeaderProps } from "./HomeHeader.types"; +import type { HomeHeaderProps, NavSize } from "./HomeHeader.types"; + +// Configuration data for testing +export const navigationItems = [ + { href: "#", text: "Use cases", extraPadding: true }, + { href: "/learn", text: "Learn" }, + { href: "#", text: "About" }, +]; + +export const avatarImages = [ + { src: getAssetPath(ASSETS.AVATAR_1), alt: "Avatar 1" }, + { src: getAssetPath(ASSETS.AVATAR_2), alt: "Avatar 2" }, + { src: getAssetPath(ASSETS.AVATAR_3), alt: "Avatar 3" }, +]; + +export const logoConfig = [ + { + breakpoint: "block sm:hidden", + size: "homeHeaderXsmall" as const, + showText: false, + }, + { + breakpoint: "hidden sm:block md:hidden", + size: "homeHeaderSm" as const, + showText: true, + }, + { + breakpoint: "hidden md:block lg:hidden", + size: "homeHeaderMd" as const, + showText: true, + }, + { + breakpoint: "hidden lg:block xl:hidden", + size: "homeHeaderLg" as const, + showText: true, + }, + { + breakpoint: "hidden xl:block", + size: "homeHeaderXl" as const, + showText: true, + }, +]; const HomeHeaderContainer = memo(() => { const pathname = usePathname(); - const { schemaData } = useSchemaData(); - // Navigation items configuration - const navigationItems = useMemo( - () => [ - { - label: "Home", - href: "/", - isActive: pathname === "/", - }, - { - label: "Learn", - href: "/learn", - isActive: pathname === "/learn", - }, - { - label: "Monitor", - href: "/monitor", - isActive: pathname === "/monitor", - }, - { - label: "Blog", - href: "/blog", - isActive: pathname?.startsWith("/blog") ?? false, - }, - ], - [pathname], - ); + // Schema markup for site navigation (home page specific) + const schemaData = { + "@context": "https://schema.org", + "@type": "WebSite", + name: "CommunityRule", + url: "https://communityrule.com", + description: "Build operating manuals for successful communities", + potentialAction: { + "@type": "SearchAction", + target: "https://communityrule.com/search?q={search_term_string}", + "query-input": "required name=search_term_string", + }, + }; - // Avatar images configuration - const avatarImages = useMemo( - () => [ - { - src: "/assets/avatar-1.svg", - alt: "User avatar 1", - }, - { - src: "/assets/avatar-2.svg", - alt: "User avatar 2", - }, - { - src: "/assets/avatar-3.svg", - alt: "User avatar 3", - }, - ], - [], - ); + const renderNavigationItems = (size: NavSize) => { + return navigationItems.map((item, index) => ( + + {item.text} + + )); + }; - // Logo configuration - const logoConfig = useMemo( - () => ({ - src: "/assets/logo.svg", - alt: "Community Rule Logo", - width: 120, - height: 32, - }), - [], - ); + const renderAvatarGroup = ( + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge", + ) => { + return ( + + {avatarImages.map((avatar, index) => ( + + ))} + + ); + }; + + const renderLoginButton = (size: NavSize) => { + return ( + + Log in + + ); + }; + + const renderCreateRuleButton = ( + buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge", + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge", + ) => { + return ( + + ); + }; + + const renderLogo = ( + size: + | "default" + | "homeHeaderXsmall" + | "homeHeaderSm" + | "homeHeaderMd" + | "homeHeaderLg" + | "homeHeaderXl" + | "header" + | "headerMd" + | "headerLg" + | "headerXl" + | "footer" + | "footerLg", + showText: boolean, + ) => { + return ; + }; return ( (() => { navigationItems={navigationItems} avatarImages={avatarImages} logoConfig={logoConfig} + renderNavigationItems={renderNavigationItems} + renderAvatarGroup={renderAvatarGroup} + renderLoginButton={renderLoginButton} + renderCreateRuleButton={renderCreateRuleButton} + renderLogo={renderLogo} /> ); }); diff --git a/app/components/HomeHeader/HomeHeader.types.ts b/app/components/HomeHeader/HomeHeader.types.ts index 48438f8..10ffaa4 100644 --- a/app/components/HomeHeader/HomeHeader.types.ts +++ b/app/components/HomeHeader/HomeHeader.types.ts @@ -1,23 +1,75 @@ +import type React from "react"; + export interface HomeHeaderProps { // Currently no props, but keeping interface for future extensibility } +export type NavSize = + | "default" + | "xsmall" + | "xsmallUseCases" + | "home" + | "homeMd" + | "homeUseCases" + | "large" + | "largeUseCases" + | "homeXlarge" + | "xlarge"; + export interface HomeHeaderViewProps { pathname: string; schemaData: object; navigationItems: Array<{ - label: string; href: string; - isActive: boolean; + text: string; + extraPadding?: boolean; }>; avatarImages: Array<{ src: string; alt: string; }>; - logoConfig: { - src: string; - alt: string; - width: number; - height: number; - }; + logoConfig: Array<{ + breakpoint: string; + size: + | "default" + | "homeHeaderXsmall" + | "homeHeaderSm" + | "homeHeaderMd" + | "homeHeaderLg" + | "homeHeaderXl" + | "header" + | "headerMd" + | "headerLg" + | "headerXl" + | "footer" + | "footerLg"; + showText: boolean; + }>; + renderNavigationItems: (size: NavSize) => React.ReactNode; + renderAvatarGroup: ( + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge", + ) => React.ReactNode; + renderLoginButton: (size: NavSize) => React.ReactNode; + renderCreateRuleButton: ( + buttonSize: "xsmall" | "small" | "medium" | "large" | "xlarge", + containerSize: "small" | "medium" | "large" | "xlarge", + avatarSize: "small" | "medium" | "large" | "xlarge", + ) => React.ReactNode; + renderLogo: ( + size: + | "default" + | "homeHeaderXsmall" + | "homeHeaderSm" + | "homeHeaderMd" + | "homeHeaderLg" + | "homeHeaderXl" + | "header" + | "headerMd" + | "headerLg" + | "headerXl" + | "footer" + | "footerLg", + showText: boolean, + ) => React.ReactNode; } diff --git a/app/components/HomeHeader/HomeHeader.view.tsx b/app/components/HomeHeader/HomeHeader.view.tsx index 06f03b8..a2b6c72 100644 --- a/app/components/HomeHeader/HomeHeader.view.tsx +++ b/app/components/HomeHeader/HomeHeader.view.tsx @@ -3,9 +3,12 @@ import { memo } from "react"; import Script from "next/script"; import Logo from "../Logo"; -import NavigationItem from "../NavigationItem"; -import AvatarContainer from "../AvatarContainer"; +import HeaderTab from "../HeaderTab"; +import MenuBar from "../MenuBar"; +import MenuBarItem from "../MenuBarItem"; import Button from "../Button"; +import AvatarContainer from "../AvatarContainer"; +import Avatar from "../Avatar"; import type { HomeHeaderViewProps } from "./HomeHeader.types"; function HomeHeaderView({ @@ -14,6 +17,11 @@ function HomeHeaderView({ navigationItems, avatarImages, logoConfig, + renderNavigationItems, + renderAvatarGroup, + renderLoginButton, + renderCreateRuleButton, + renderLogo, }: HomeHeaderViewProps) { return ( <> @@ -22,41 +30,91 @@ function HomeHeaderView({ type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(schemaData) }} /> -
-
-
-
- - +
+
); diff --git a/app/components/LogoWall/LogoWall.container.tsx b/app/components/LogoWall/LogoWall.container.tsx index 54ab1d6..0b25b91 100644 --- a/app/components/LogoWall/LogoWall.container.tsx +++ b/app/components/LogoWall/LogoWall.container.tsx @@ -6,40 +6,40 @@ import type { LogoWallProps } from "./LogoWall.types"; const defaultLogos = [ { - src: "/assets/logo-1.svg", - alt: "Partner Logo 1", - width: 120, - height: 40, + src: "/assets/Section/Logo_FoodNotBombs.png", + alt: "Food Not Bombs", + size: "h-11 lg:h-14 xl:h-[70px]", + order: "order-1 sm:order-4", // Mobile: row 1 col 1, SM: row 2 col 1 (bottom left) }, { - src: "/assets/logo-2.svg", - alt: "Partner Logo 2", - width: 120, - height: 40, + src: "/assets/Section/Logo_StartCOOP.png", + alt: "Start COOP", + size: "h-[42px] lg:h-[53px] xl:h-[66px]", + order: "order-2 sm:order-2", // Mobile: row 1 col 2, SM: row 1 col 2 (top middle) }, { - src: "/assets/logo-3.svg", - alt: "Partner Logo 3", - width: 120, - height: 40, + src: "/assets/Section/Logo_Metagov.png", + alt: "Metagov", + size: "h-6 lg:h-8 xl:h-[41px]", + order: "order-3 sm:order-1", // Mobile: row 2 col 1, SM: row 1 col 1 (top left) }, { - src: "/assets/logo-4.svg", - alt: "Partner Logo 4", - width: 120, - height: 40, + src: "/assets/Section/Logo_OpenCivics.png", + alt: "Open Civics", + size: "h-8 lg:h-10 xl:h-[50px]", + order: "order-4 sm:order-5 md:order-6", // Mobile: row 2 col 2, SM: row 2 col 2, MD: swapped with Mutual Aid CO }, { - src: "/assets/logo-5.svg", - alt: "Partner Logo 5", - width: 120, - height: 40, + src: "/assets/Section/Logo_MutualAidCO.png", + alt: "Mutual Aid CO", + size: "h-11 lg:h-14 xl:h-[70px]", + order: "order-5 sm:order-6 md:order-5", // Mobile: row 3 col 1, SM: row 2 col 3, MD: swapped with OpenCivics }, { - src: "/assets/logo-6.svg", - alt: "Partner Logo 6", - width: 120, - height: 40, + src: "/assets/Section/Logo_CUBoulder.png", + alt: "CU Boulder", + size: "h-10 lg:h-12 xl:h-[60px]", + order: "order-6 sm:order-3", // Mobile: row 3 col 2, SM: row 1 col 3 (top right) }, ]; @@ -47,7 +47,10 @@ const LogoWallContainer = memo( ({ logos, className = "" }) => { const [isVisible, setIsVisible] = useState(false); - const displayLogos = useMemo(() => logos || defaultLogos, [logos]); + const displayLogos = useMemo( + () => (logos && logos.length > 0 ? logos : defaultLogos), + [logos], + ); useEffect(() => { // Trigger fade-in animation after component mounts diff --git a/app/components/LogoWall/LogoWall.types.ts b/app/components/LogoWall/LogoWall.types.ts index 0291a48..b186101 100644 --- a/app/components/LogoWall/LogoWall.types.ts +++ b/app/components/LogoWall/LogoWall.types.ts @@ -4,6 +4,8 @@ export interface LogoWallProps { alt: string; width?: number; height?: number; + size?: string; + order?: string; }>; className?: string; } @@ -15,6 +17,8 @@ export interface LogoWallViewProps { alt: string; width?: number; height?: number; + size?: string; + order?: string; }>; className: string; } diff --git a/app/components/LogoWall/LogoWall.view.tsx b/app/components/LogoWall/LogoWall.view.tsx index b78abe3..2384436 100644 --- a/app/components/LogoWall/LogoWall.view.tsx +++ b/app/components/LogoWall/LogoWall.view.tsx @@ -1,30 +1,56 @@ +"use client"; + import { memo } from "react"; import Image from "next/image"; import type { LogoWallViewProps } from "./LogoWall.types"; -function LogoWallView({ isVisible, displayLogos, className }: LogoWallViewProps) { +function LogoWallView({ + isVisible, + displayLogos, + className, +}: LogoWallViewProps) { return ( -
- {displayLogos.map((logo, index) => ( +
+ {/* Label */} +

+ Trusted by leading cooperators +

+ + {/* Logo Grid Container */}
- {logo.alt} +
+ {displayLogos.map((logo, index) => ( +
+ {logo.alt} +
+ ))} +
- ))} -
+
+ ); } diff --git a/app/components/NavigationItem/NavigationItem.container.tsx b/app/components/NavigationItem/NavigationItem.container.tsx index 2874205..7e84509 100644 --- a/app/components/NavigationItem/NavigationItem.container.tsx +++ b/app/components/NavigationItem/NavigationItem.container.tsx @@ -12,6 +12,7 @@ const NavigationItemContainer = memo( size = "default", className = "", disabled = false, + isActive = false, ...props }) => { // Variant styles @@ -43,7 +44,12 @@ const NavigationItemContainer = memo( finalVariant = "default"; // The disabled state is handled by disabled: utilities } - const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${className}`; + // Active state styling + const activeStyles = isActive + ? "!border-[var(--color-content-default-brand-primary)] !text-[var(--color-content-default-brand-primary)]" + : ""; + + const combinedStyles = `${baseStyles} ${variantStyles[finalVariant]} ${activeStyles} ${className}`; return ( { + extends Omit, "isActive"> { href?: string; children?: React.ReactNode; variant?: "default"; size?: "default" | "xsmall"; className?: string; disabled?: boolean; + isActive?: boolean; } export interface NavigationItemViewProps { diff --git a/app/components/QuoteBlock/QuoteBlock.container.tsx b/app/components/QuoteBlock/QuoteBlock.container.tsx index 8d0e007..eb3cf5d 100644 --- a/app/components/QuoteBlock/QuoteBlock.container.tsx +++ b/app/components/QuoteBlock/QuoteBlock.container.tsx @@ -1,134 +1,137 @@ "use client"; -import { memo, useState, useId } from "react"; -import { logger } from "../../lib/logger"; +import { memo, useState } from "react"; +import { logger } from "../../../lib/logger"; import QuoteBlockView from "./QuoteBlock.view"; -import type { QuoteBlockProps } from "./QuoteBlock.types"; +import type { QuoteBlockProps, VariantConfig } from "./QuoteBlock.types"; const QuoteBlockContainer = memo( ({ - quote, - author, - authorRole, - authorImage, - variant = "default", + variant = "standard", className = "", + quote = "The rules of decision-making must be open and available to everyone, and this can happen only if they are formalized.", + author = "Jo Freeman", + source = "The Tyranny of Structurelessness", + avatarSrc = "/assets/Quote_Avatar.svg", + id, + fallbackAvatarSrc = "/assets/Quote_Avatar.svg", + onError, }) => { const [imageError, setImageError] = useState(false); const [imageLoading, setImageLoading] = useState(true); - const quoteId = useId(); - // Variant configuration - const variantConfig = { - default: { - container: - "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-064)]", - quote: "text-[var(--color-content-default-primary)]", - author: "text-[var(--color-content-default-secondary)]", - authorRole: "text-[var(--color-content-default-tertiary)]", - }, - inverse: { - container: - "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-064)] bg-[var(--color-surface-inverse-primary)]", - quote: "text-[var(--color-content-inverse-primary)]", - author: "text-[var(--color-content-inverse-secondary)]", - authorRole: "text-[var(--color-content-inverse-tertiary)]", - }, + // Variant configurations + const variants: Record = { compact: { container: - "py-[var(--spacing-scale-016)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-032)]", - quote: "text-[var(--color-content-default-primary)]", - author: "text-[var(--color-content-default-secondary)]", - authorRole: "text-[var(--color-content-default-tertiary)]", + "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)]", + card: "py-[var(--spacing-scale-032)] px-[var(--spacing-scale-016)] md:py-[var(--spacing-scale-040)] md:px-[var(--spacing-scale-024)] rounded-[var(--radius-measures-radius-small)]", + gap: "gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]", + avatarGap: "gap-[var(--spacing-scale-012)]", + avatar: "w-[48px] h-[48px] md:w-[64px] md:h-[64px]", + quote: "text-[16px] leading-[120%] md:text-[20px] md:leading-[110%]", + author: "text-[10px] leading-[120%] md:text-[12px]", + source: "text-[10px] leading-[120%] md:text-[12px]", + showDecor: false, + }, + standard: { + container: + "md:py-[var(--spacing-scale-032)] md:px-[var(--spacing-scale-016)] lg:p-[var(--spacing-scale-064)]", + card: "py-[var(--spacing-scale-064)] px-[var(--spacing-scale-020)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-048)] md:rounded-[var(--radius-measures-radius-medium)] lg:py-[var(--spacing-scale-064)] lg:pl-[120px] lg:pr-[320px]", + gap: "gap-[var(--spacing-scale-024)] md:gap-[var(--spacing-scale-048)] lg:gap-[var(--spacing-scale-064)] xl:gap-[105px]", + avatarGap: + "gap-[var(--spacing-scale-020)] lg:gap-[var(--spacing-scale-018)] xl:gap-[var(--spacing-scale-032)]", + avatar: + "md:w-[120px] md:h-[120px] lg:w-[150px] lg:h-[150px] xl:w-[200px] xl:h-[200px]", + quote: + "text-[18px] leading-[120%] md:text-[36px] md:leading-[110%] md:tracking-[0px] lg:text-[52px] xl:text-[64px]", + author: + "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", + source: + "text-[12px] leading-[120%] md:text-[18px] md:leading-[120%] md:tracking-[0.24px] lg:text-[24px] xl:text-[32px]", + showDecor: true, + }, + extended: { + container: + "py-[var(--spacing-scale-048)] px-[var(--spacing-scale-024)] md:py-[var(--spacing-scale-064)] md:px-[var(--spacing-scale-032)] lg:py-[var(--spacing-scale-080)] lg:px-[var(--spacing-scale-048)]", + card: "py-[var(--spacing-scale-080)] px-[var(--spacing-scale-032)] md:py-[var(--spacing-scale-096)] md:px-[var(--spacing-scale-064)] md:rounded-[var(--radius-measures-radius-large)] lg:py-[var(--spacing-scale-112)] lg:pl-[160px] lg:pr-[400px]", + gap: "gap-[var(--spacing-scale-032)] md:gap-[var(--spacing-scale-064)] lg:gap-[var(--spacing-scale-080)] xl:gap-[140px]", + avatarGap: + "gap-[var(--spacing-scale-032)] lg:gap-[var(--spacing-scale-040)] xl:gap-[var(--spacing-scale-048)]", + avatar: + "w-[80px] h-[80px] md:w-[140px] md:h-[140px] lg:w-[180px] lg:h-[180px] xl:w-[240px] xl:h-[240px]", + quote: + "text-[20px] leading-[120%] md:text-[40px] md:leading-[110%] md:tracking-[0px] lg:text-[60px] xl:text-[72px]", + author: + "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", + source: + "text-[14px] leading-[120%] md:text-[20px] md:leading-[120%] md:tracking-[0.24px] lg:text-[28px] xl:text-[36px]", + showDecor: true, }, }; - const config = variantConfig[variant]; + const config = variants[variant] || variants.standard; - const containerClasses = ` - relative - flex - flex-col - gap-[var(--spacing-scale-024)] - ${config.container} - ` - .trim() - .replace(/\s+/g, " "); + // Use provided ID or generate a stable one based on content + const baseId = id || `quote-${author.toLowerCase().replace(/\s+/g, "-")}`; + const quoteId = `${baseId}-content`; + const authorId = `${baseId}-author`; - const quoteClasses = ` - text-[18px] - md:text-[24px] - leading-[28px] - md:leading-[36px] - font-medium - ${config.quote} - ` - .trim() - .replace(/\s+/g, " "); + // Error handling functions + const handleImageError = (error: unknown) => { + logger.warn( + `QuoteBlock: Failed to load avatar image for ${author}:`, + error, + ); + setImageError(true); + setImageLoading(false); - const authorClasses = ` - text-[14px] - md:text-[16px] - leading-[20px] - md:leading-[24px] - font-semibold - not-italic - ${config.author} - ` - .trim() - .replace(/\s+/g, " "); - - const authorRoleClasses = ` - text-[12px] - md:text-[14px] - leading-[16px] - md:leading-[20px] - font-normal - ${config.authorRole} - ` - .trim() - .replace(/\s+/g, " "); - - const imageContainerClasses = ` - w-[var(--measures-sizing-048)] - h-[var(--measures-sizing-048)] - rounded-full - overflow-hidden - shrink-0 - ` - .trim() - .replace(/\s+/g, " "); + // Call error callback if provided + if (onError) { + onError({ + type: "image_load_error", + message: `Failed to load avatar for ${author}`, + author, + avatarSrc, + error, + }); + } + }; const handleImageLoad = () => { setImageLoading(false); setImageError(false); }; - const handleImageError = () => { - setImageError(true); - setImageLoading(false); - logger.warn("QuoteBlock: Failed to load author image", { - authorImage, - author, - }); - }; + // Validate required props + if (!quote || !author) { + logger.error("QuoteBlock: Missing required props (quote or author)"); + if (onError) { + onError({ + type: "missing_props", + message: "QuoteBlock requires quote and author props", + quote: !!quote, + author, + }); + } + return null; // Don't render if missing required props + } + + // Determine which avatar to use + const currentAvatarSrc = imageError ? fallbackAvatarSrc : avatarSrc; return ( diff --git a/app/components/QuoteBlock/QuoteBlock.types.ts b/app/components/QuoteBlock/QuoteBlock.types.ts index 64d944e..192f22b 100644 --- a/app/components/QuoteBlock/QuoteBlock.types.ts +++ b/app/components/QuoteBlock/QuoteBlock.types.ts @@ -1,27 +1,45 @@ export interface QuoteBlockProps { - quote: string; - author?: string; - authorRole?: string; - authorImage?: string; - variant?: "default" | "inverse" | "compact"; + variant?: "compact" | "standard" | "extended"; className?: string; + quote?: string; + author?: string; + source?: string; + avatarSrc?: string; + id?: string; + fallbackAvatarSrc?: string; + onError?: (_error: { + type: string; + message: string; + author?: string; + avatarSrc?: string; + error?: unknown; + quote?: boolean; + }) => void; +} + +export interface VariantConfig { + container: string; + card: string; + gap: string; + avatarGap: string; + avatar: string; + quote: string; + author: string; + source: string; + showDecor: boolean; } export interface QuoteBlockViewProps { - quoteId: string; - quote: string; - author?: string; - authorRole?: string; - authorImage?: string; - variant: "default" | "inverse" | "compact"; className: string; + quote: string; + author: string; + source?: string; + quoteId: string; + authorId: string; + config: VariantConfig; imageError: boolean; imageLoading: boolean; - containerClasses: string; - quoteClasses: string; - authorClasses: string; - authorRoleClasses: string; - imageContainerClasses: string; + currentAvatarSrc: string; onImageLoad: () => void; - onImageError: () => void; + onImageError: (error: unknown) => void; } diff --git a/app/components/QuoteBlock/QuoteBlock.view.tsx b/app/components/QuoteBlock/QuoteBlock.view.tsx index bee0c09..82933ce 100644 --- a/app/components/QuoteBlock/QuoteBlock.view.tsx +++ b/app/components/QuoteBlock/QuoteBlock.view.tsx @@ -1,64 +1,148 @@ +"use client"; + import { memo } from "react"; import Image from "next/image"; import QuoteDecor from "../QuoteDecor"; import type { QuoteBlockViewProps } from "./QuoteBlock.types"; function QuoteBlockView({ - quoteId, + className, quote, author, - authorRole, - authorImage, - variant, - className, + source, + quoteId, + authorId, + config, imageError, imageLoading, - containerClasses, - quoteClasses, - authorClasses, - authorRoleClasses, - imageContainerClasses, + currentAvatarSrc, onImageLoad, onImageError, }: QuoteBlockViewProps) { return ( -
- -
-

{quote}

- {(author || authorRole) && ( -
- {authorImage && !imageError && ( -
- {imageLoading ? ( -
- ) : ( - {author - )} -
- )} -
- {author && {author}} - {authorRole && ( - {authorRole} +
+ {/* Background with noise texture */} +
#grain\')', + }} + /> + + {/* DECORATIONS (behind content) */} + {config.showDecor && ( +
+ ); } diff --git a/app/components/QuoteDecor.tsx b/app/components/QuoteDecor.tsx index 5e24502..a1d16dd 100644 --- a/app/components/QuoteDecor.tsx +++ b/app/components/QuoteDecor.tsx @@ -9,11 +9,12 @@ interface QuoteDecorProps { const QuoteDecor = memo(({ className = "" }) => { return (