[dyad] Added status indicators to tabs - wrote 4 file(s)
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
TooltipTrigger,
|
TooltipTrigger,
|
||||||
} from "@/components/ui/tooltip";
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { getLengthIndicatorColor } from "@/lib/analysis";
|
||||||
|
|
||||||
interface LengthIndicatorProps {
|
interface LengthIndicatorProps {
|
||||||
length: number;
|
length: number;
|
||||||
@@ -24,20 +25,24 @@ const DESCRIPTION_THRESHOLDS = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function LengthIndicator({ length, type }: LengthIndicatorProps) {
|
export function LengthIndicator({ length, type }: LengthIndicatorProps) {
|
||||||
|
const color = getLengthIndicatorColor(length, type);
|
||||||
|
|
||||||
|
const colorClassMap = {
|
||||||
|
green: "bg-green-500",
|
||||||
|
yellow: "bg-yellow-500",
|
||||||
|
red: "bg-red-500",
|
||||||
|
gray: "bg-gray-400",
|
||||||
|
};
|
||||||
|
|
||||||
const thresholds =
|
const thresholds =
|
||||||
type === "title" ? TITLE_THRESHOLDS : DESCRIPTION_THRESHOLDS;
|
type === "title" ? TITLE_THRESHOLDS : DESCRIPTION_THRESHOLDS;
|
||||||
|
|
||||||
let colorClass = "bg-red-500";
|
|
||||||
let tooltipText = "Length is not optimal";
|
let tooltipText = "Length is not optimal";
|
||||||
|
|
||||||
if (length === 0) {
|
if (length === 0) {
|
||||||
tooltipText = "Not found";
|
tooltipText = "Not found";
|
||||||
colorClass = "bg-gray-400";
|
|
||||||
} else if (length >= thresholds.good.min && length <= thresholds.good.max) {
|
} else if (length >= thresholds.good.min && length <= thresholds.good.max) {
|
||||||
colorClass = "bg-green-500";
|
|
||||||
tooltipText = "Optimal length";
|
tooltipText = "Optimal length";
|
||||||
} else if (length >= thresholds.ok.min && length <= thresholds.ok.max) {
|
} else if (length >= thresholds.ok.min && length <= thresholds.ok.max) {
|
||||||
colorClass = "bg-yellow-500";
|
|
||||||
tooltipText = "Length could be improved";
|
tooltipText = "Length could be improved";
|
||||||
} else if (length < thresholds.ok.min) {
|
} else if (length < thresholds.ok.min) {
|
||||||
tooltipText = "Too short";
|
tooltipText = "Too short";
|
||||||
@@ -49,7 +54,7 @@ export function LengthIndicator({ length, type }: LengthIndicatorProps) {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div className={cn("h-3 w-3 rounded-full", colorClass)} />
|
<div className={cn("h-3 w-3 rounded-full", colorClassMap[color])} />
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
<p>
|
<p>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
@@ -14,7 +14,11 @@ import {
|
|||||||
ImageOff,
|
ImageOff,
|
||||||
Search,
|
Search,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { extractMetaData, type HeadlineNode, type ImageAltData } from "@/app/actions";
|
import {
|
||||||
|
extractMetaData,
|
||||||
|
type HeadlineNode,
|
||||||
|
type ImageAltData,
|
||||||
|
} from "@/app/actions";
|
||||||
import { LengthIndicator } from "./length-indicator";
|
import { LengthIndicator } from "./length-indicator";
|
||||||
import { CopyButton } from "./copy-button";
|
import { CopyButton } from "./copy-button";
|
||||||
import { SerpPreview } from "./serp-preview";
|
import { SerpPreview } from "./serp-preview";
|
||||||
@@ -23,6 +27,8 @@ import { FaqDisplay } from "./faq-display";
|
|||||||
import { HeadlineTree } from "./headline-tree";
|
import { HeadlineTree } from "./headline-tree";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
import { ImageAltDisplay } from "./image-alt-display";
|
import { ImageAltDisplay } from "./image-alt-display";
|
||||||
|
import { TabIndicator } from "./tab-indicator";
|
||||||
|
import { getLengthIndicatorColor, type IndicatorColor } from "@/lib/analysis";
|
||||||
|
|
||||||
interface MetaData {
|
interface MetaData {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -57,6 +63,62 @@ export function MetaForm() {
|
|||||||
}
|
}
|
||||||
}, [metaData]);
|
}, [metaData]);
|
||||||
|
|
||||||
|
const tabColors = useMemo(() => {
|
||||||
|
if (!metaData) return null;
|
||||||
|
|
||||||
|
// Analysis Tab
|
||||||
|
const titleStatus = getLengthIndicatorColor(editableTitle.length, "title");
|
||||||
|
const descStatus = getLengthIndicatorColor(
|
||||||
|
editableDescription.length,
|
||||||
|
"description"
|
||||||
|
);
|
||||||
|
const analysisStatuses: IndicatorColor[] = [titleStatus, descStatus];
|
||||||
|
let analysisColor: IndicatorColor;
|
||||||
|
if (
|
||||||
|
analysisStatuses.includes("red") ||
|
||||||
|
analysisStatuses.includes("gray")
|
||||||
|
) {
|
||||||
|
analysisColor = "red";
|
||||||
|
} else if (analysisStatuses.includes("yellow")) {
|
||||||
|
analysisColor = "yellow";
|
||||||
|
} else {
|
||||||
|
analysisColor = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headlines Tab
|
||||||
|
let headlinesColor: IndicatorColor = "gray";
|
||||||
|
if (metaData.headlines && metaData.headlines.length > 0) {
|
||||||
|
const h1s = metaData.headlines.filter((h) => h.tag === "h1");
|
||||||
|
if (h1s.length === 0) {
|
||||||
|
headlinesColor = "red";
|
||||||
|
} else if (h1s.length > 1) {
|
||||||
|
headlinesColor = "yellow";
|
||||||
|
} else {
|
||||||
|
headlinesColor = "green";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Images Tab
|
||||||
|
let imagesColor: IndicatorColor = "gray";
|
||||||
|
if (metaData.images && metaData.images.length > 0) {
|
||||||
|
const missingAltCount = metaData.images.filter((img) => !img.alt).length;
|
||||||
|
imagesColor = missingAltCount > 0 ? "red" : "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
// FAQ Tab
|
||||||
|
let faqColor: IndicatorColor = "gray";
|
||||||
|
if (metaData.faq && metaData.faq.length > 0) {
|
||||||
|
faqColor = "green";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
analysis: analysisColor,
|
||||||
|
headlines: headlinesColor,
|
||||||
|
images: imagesColor,
|
||||||
|
faq: faqColor,
|
||||||
|
};
|
||||||
|
}, [metaData, editableTitle, editableDescription]);
|
||||||
|
|
||||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -149,15 +211,27 @@ export function MetaForm() {
|
|||||||
{!loading && metaData && (
|
{!loading && metaData && (
|
||||||
<Tabs defaultValue="analysis" className="w-full">
|
<Tabs defaultValue="analysis" className="w-full">
|
||||||
<TabsList className="mb-4">
|
<TabsList className="mb-4">
|
||||||
<TabsTrigger value="analysis">Meta Analysis</TabsTrigger>
|
<TabsTrigger value="analysis">
|
||||||
|
{tabColors && <TabIndicator color={tabColors.analysis} />}
|
||||||
|
Meta Analysis
|
||||||
|
</TabsTrigger>
|
||||||
{metaData.headlines && metaData.headlines.length > 0 && (
|
{metaData.headlines && metaData.headlines.length > 0 && (
|
||||||
<TabsTrigger value="headlines">Headlines</TabsTrigger>
|
<TabsTrigger value="headlines">
|
||||||
|
{tabColors && <TabIndicator color={tabColors.headlines} />}
|
||||||
|
Headlines
|
||||||
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{metaData.images && metaData.images.length > 0 && (
|
{metaData.images && metaData.images.length > 0 && (
|
||||||
<TabsTrigger value="images">Images</TabsTrigger>
|
<TabsTrigger value="images">
|
||||||
|
{tabColors && <TabIndicator color={tabColors.images} />}
|
||||||
|
Images
|
||||||
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
{metaData.faq && metaData.faq.length > 0 && (
|
{metaData.faq && metaData.faq.length > 0 && (
|
||||||
<TabsTrigger value="faq">FAQ</TabsTrigger>
|
<TabsTrigger value="faq">
|
||||||
|
{tabColors && <TabIndicator color={tabColors.faq} />}
|
||||||
|
FAQ
|
||||||
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
|
|||||||
26
src/components/tab-indicator.tsx
Normal file
26
src/components/tab-indicator.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import type { IndicatorColor } from "@/lib/analysis";
|
||||||
|
|
||||||
|
interface TabIndicatorProps {
|
||||||
|
color: IndicatorColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TabIndicator({ color }: TabIndicatorProps) {
|
||||||
|
const colorClassMap = {
|
||||||
|
green: "bg-green-500",
|
||||||
|
yellow: "bg-yellow-500",
|
||||||
|
red: "bg-red-500",
|
||||||
|
gray: "bg-gray-400",
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"h-2 w-2 rounded-full mr-2 flex-shrink-0",
|
||||||
|
colorClassMap[color]
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
30
src/lib/analysis.ts
Normal file
30
src/lib/analysis.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
export type IndicatorColor = "green" | "yellow" | "red" | "gray";
|
||||||
|
|
||||||
|
const TITLE_THRESHOLDS = {
|
||||||
|
good: { min: 30, max: 60 },
|
||||||
|
ok: { min: 15, max: 70 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const DESCRIPTION_THRESHOLDS = {
|
||||||
|
good: { min: 120, max: 158 },
|
||||||
|
ok: { min: 50, max: 170 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getLengthIndicatorColor(
|
||||||
|
length: number,
|
||||||
|
type: "title" | "description"
|
||||||
|
): IndicatorColor {
|
||||||
|
const thresholds =
|
||||||
|
type === "title" ? TITLE_THRESHOLDS : DESCRIPTION_THRESHOLDS;
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
return "gray";
|
||||||
|
}
|
||||||
|
if (length >= thresholds.good.min && length <= thresholds.good.max) {
|
||||||
|
return "green";
|
||||||
|
}
|
||||||
|
if (length >= thresholds.ok.min && length <= thresholds.ok.max) {
|
||||||
|
return "yellow";
|
||||||
|
}
|
||||||
|
return "red";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user