Finish migrating components

This commit is contained in:
adilallo
2026-01-29 17:59:11 -07:00
parent b5735bb2ad
commit 539f6c62e3
79 changed files with 2449 additions and 1730 deletions
@@ -0,0 +1,141 @@
"use client";
import { memo, useState, useId } from "react";
import { logger } from "../../lib/logger";
import QuoteBlockView from "./QuoteBlock.view";
import type { QuoteBlockProps } from "./QuoteBlock.types";
const QuoteBlockContainer = memo<QuoteBlockProps>(
({
quote,
author,
authorRole,
authorImage,
variant = "default",
className = "",
}) => {
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)]",
},
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)]",
},
};
const config = variantConfig[variant];
const containerClasses = `
relative
flex
flex-col
gap-[var(--spacing-scale-024)]
${config.container}
`
.trim()
.replace(/\s+/g, " ");
const quoteClasses = `
text-[18px]
md:text-[24px]
leading-[28px]
md:leading-[36px]
font-medium
${config.quote}
`
.trim()
.replace(/\s+/g, " ");
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, " ");
const handleImageLoad = () => {
setImageLoading(false);
setImageError(false);
};
const handleImageError = () => {
setImageError(true);
setImageLoading(false);
logger.warn("QuoteBlock: Failed to load author image", {
authorImage,
author,
});
};
return (
<QuoteBlockView
quoteId={quoteId}
quote={quote}
author={author}
authorRole={authorRole}
authorImage={authorImage}
variant={variant}
className={className}
imageError={imageError}
imageLoading={imageLoading}
containerClasses={containerClasses}
quoteClasses={quoteClasses}
authorClasses={authorClasses}
authorRoleClasses={authorRoleClasses}
imageContainerClasses={imageContainerClasses}
onImageLoad={handleImageLoad}
onImageError={handleImageError}
/>
);
},
);
QuoteBlockContainer.displayName = "QuoteBlock";
export default QuoteBlockContainer;
@@ -0,0 +1,27 @@
export interface QuoteBlockProps {
quote: string;
author?: string;
authorRole?: string;
authorImage?: string;
variant?: "default" | "inverse" | "compact";
className?: string;
}
export interface QuoteBlockViewProps {
quoteId: string;
quote: string;
author?: string;
authorRole?: string;
authorImage?: string;
variant: "default" | "inverse" | "compact";
className: string;
imageError: boolean;
imageLoading: boolean;
containerClasses: string;
quoteClasses: string;
authorClasses: string;
authorRoleClasses: string;
imageContainerClasses: string;
onImageLoad: () => void;
onImageError: () => void;
}
@@ -0,0 +1,67 @@
import { memo } from "react";
import Image from "next/image";
import QuoteDecor from "../QuoteDecor";
import type { QuoteBlockViewProps } from "./QuoteBlock.types";
function QuoteBlockView({
quoteId,
quote,
author,
authorRole,
authorImage,
variant,
className,
imageError,
imageLoading,
containerClasses,
quoteClasses,
authorClasses,
authorRoleClasses,
imageContainerClasses,
onImageLoad,
onImageError,
}: QuoteBlockViewProps) {
return (
<blockquote
id={quoteId}
className={`${containerClasses} ${className}`}
aria-label={author ? `Quote by ${author}` : "Quote"}
>
<QuoteDecor variant={variant} />
<div className="flex flex-col gap-[var(--spacing-scale-016)] md:gap-[var(--spacing-scale-024)]">
<p className={quoteClasses}>{quote}</p>
{(author || authorRole) && (
<div className="flex items-center gap-[var(--spacing-scale-016)]">
{authorImage && !imageError && (
<div className={imageContainerClasses}>
{imageLoading ? (
<div className="w-full h-full bg-[var(--color-surface-default-secondary)] animate-pulse rounded-full" />
) : (
<Image
src={authorImage}
alt={author ? `${author}'s profile picture` : "Author"}
width={48}
height={48}
className="rounded-full object-cover"
onLoad={onImageLoad}
onError={onImageError}
/>
)}
</div>
)}
<div className="flex flex-col">
{author && <cite className={authorClasses}>{author}</cite>}
{authorRole && (
<span className={authorRoleClasses}>{authorRole}</span>
)}
</div>
</div>
)}
</div>
</blockquote>
);
}
QuoteBlockView.displayName = "QuoteBlockView";
export default memo(QuoteBlockView);
+2
View File
@@ -0,0 +1,2 @@
export { default } from "./QuoteBlock.container";
export type { QuoteBlockProps } from "./QuoteBlock.types";