[dyad] Add headline hierarchy validation - wrote 1 file(s)
This commit is contained in:
@@ -4,7 +4,12 @@ import { useState } from "react";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import type { HeadlineNode } from "@/app/actions";
|
||||
import { KeywordHighlighter } from "./keyword-highlighter";
|
||||
import { ChevronDown, ChevronsUp, ChevronsDown } from "lucide-react";
|
||||
import {
|
||||
ChevronDown,
|
||||
ChevronsUp,
|
||||
ChevronsDown,
|
||||
AlertTriangle,
|
||||
} from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
@@ -36,6 +41,13 @@ const getCollapsiblePaths = (
|
||||
return paths;
|
||||
};
|
||||
|
||||
const HierarchyWarning = ({ message }: { message: string }) => (
|
||||
<div className="flex items-center gap-3 p-4 text-sm text-destructive-foreground bg-destructive/80 border-b">
|
||||
<AlertTriangle className="h-5 w-5 flex-shrink-0" />
|
||||
<p className="font-medium">{message}</p>
|
||||
</div>
|
||||
);
|
||||
|
||||
const HeadlineNodeDisplay = ({
|
||||
node,
|
||||
level,
|
||||
@@ -43,30 +55,44 @@ const HeadlineNodeDisplay = ({
|
||||
keyword,
|
||||
collapsedStates,
|
||||
onToggle,
|
||||
parentLevel,
|
||||
isDuplicateH1,
|
||||
}: {
|
||||
node: HeadlineNode;
|
||||
level: number;
|
||||
path: string;
|
||||
path:string;
|
||||
keyword?: string | null;
|
||||
collapsedStates: Record<string, boolean>;
|
||||
onToggle: (path: string) => void;
|
||||
parentLevel: number;
|
||||
isDuplicateH1: boolean;
|
||||
}) => {
|
||||
const isCollapsible =
|
||||
node.level === 2 && node.children && node.children.length > 0;
|
||||
const isCollapsed = collapsedStates[path];
|
||||
|
||||
return (
|
||||
<>
|
||||
const levelSkipped = parentLevel > 0 && node.level > parentLevel + 1;
|
||||
const hasError = isDuplicateH1 || levelSkipped;
|
||||
|
||||
let errorTooltip: string | null = null;
|
||||
if (isDuplicateH1) {
|
||||
errorTooltip = "Duplicate H1 tag. Only one H1 is recommended per page.";
|
||||
} else if (levelSkipped) {
|
||||
errorTooltip = `Incorrect hierarchy. An H${node.level} should not directly follow an H${parentLevel}.`;
|
||||
}
|
||||
|
||||
const content = (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center gap-3 py-2.5 pr-4 border-t",
|
||||
isCollapsible && "cursor-pointer hover:bg-muted/50"
|
||||
isCollapsible && "cursor-pointer hover:bg-muted/50",
|
||||
hasError && "bg-destructive/10"
|
||||
)}
|
||||
style={{ paddingLeft: `${16 + level * 24}px` }}
|
||||
onClick={() => isCollapsible && onToggle(path)}
|
||||
>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
variant={hasError ? "destructive" : "secondary"}
|
||||
className="uppercase w-12 flex-shrink-0 justify-center font-mono"
|
||||
>
|
||||
{node.tag}
|
||||
@@ -86,6 +112,22 @@ const HeadlineNodeDisplay = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{errorTooltip ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{content}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{errorTooltip}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
content
|
||||
)}
|
||||
{node.children &&
|
||||
(!isCollapsible || !isCollapsed) &&
|
||||
node.children.map((child, index) => (
|
||||
@@ -97,6 +139,8 @@ const HeadlineNodeDisplay = ({
|
||||
keyword={keyword}
|
||||
collapsedStates={collapsedStates}
|
||||
onToggle={onToggle}
|
||||
parentLevel={node.level}
|
||||
isDuplicateH1={false}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -139,9 +183,17 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
|
||||
};
|
||||
|
||||
const hasCollapsibleNodes = Object.keys(collapsedStates).length > 0;
|
||||
const h1Count = headlines.filter((h) => h.tag === "h1").length;
|
||||
const firstH1Index = headlines.findIndex((h) => h.tag === "h1");
|
||||
|
||||
return (
|
||||
<div className="w-full rounded-lg overflow-hidden">
|
||||
{h1Count === 0 && (
|
||||
<HierarchyWarning message="No H1 tag found. Every page should have exactly one H1." />
|
||||
)}
|
||||
{h1Count > 1 && (
|
||||
<HierarchyWarning message="Multiple H1 tags found. Only one H1 is recommended per page." />
|
||||
)}
|
||||
{hasCollapsibleNodes && (
|
||||
<div className="flex items-center justify-between p-4 border-b bg-muted/50">
|
||||
<h4 className="font-semibold text-sm text-muted-foreground">
|
||||
@@ -197,6 +249,8 @@ export function HeadlineTree({ headlines, keyword }: HeadlineTreeProps) {
|
||||
keyword={keyword}
|
||||
collapsedStates={collapsedStates}
|
||||
onToggle={handleToggle}
|
||||
parentLevel={0}
|
||||
isDuplicateH1={headline.tag === "h1" && index !== firstH1Index}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user