361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState, useMemo } from "react";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { extractMetaData } from "@/app/actions";
|
|
import { ResultsSkeleton } from "./results-skeleton";
|
|
import { FaqDisplay } from "./faq-display";
|
|
import { HeadlineTree } from "./headline-tree";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { ImageAltDisplay } from "./image-alt-display";
|
|
import { TabIndicator } from "./tab-indicator";
|
|
import { getLengthIndicatorColor, type IndicatorColor } from "@/lib/analysis";
|
|
import { SchemaDisplay } from "./schema-display";
|
|
import { MetaFormInputs } from "./meta-form-inputs";
|
|
import { AnalysisTab } from "./analysis-tab";
|
|
import { SocialTab } from "./social-tab";
|
|
import { LinksDisplay } from "./links-display";
|
|
import { SystemDisplay } from "./system-display";
|
|
import { TrackingDisplay } from "./tracking-display";
|
|
import type { MetaData } from "@/lib/types";
|
|
|
|
export function MetaForm() {
|
|
const [url, setUrl] = useState("");
|
|
const [keyword, setKeyword] = useState("");
|
|
const [loading, setLoading] = useState(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [metaData, setMetaData] = useState<MetaData | null>(null);
|
|
|
|
const [isEditingTitle, setIsEditingTitle] = useState(false);
|
|
const [isEditingDescription, setIsEditingDescription] = useState(false);
|
|
|
|
const [editableTitle, setEditableTitle] = useState("");
|
|
const [editableDescription, setEditableDescription] = useState("");
|
|
const [imageError, setImageError] = useState(false);
|
|
|
|
useEffect(() => {
|
|
if (metaData) {
|
|
setEditableTitle(metaData.title);
|
|
setEditableDescription(metaData.description);
|
|
setImageError(false);
|
|
}
|
|
}, [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 totalImages = metaData.images.length;
|
|
const missingAltCount = metaData.images.filter((img) => !img.alt).length;
|
|
|
|
if (missingAltCount === 0) {
|
|
imagesColor = "green";
|
|
} else {
|
|
const missingPercentage = missingAltCount / totalImages;
|
|
if (missingPercentage > 0.5) {
|
|
imagesColor = "red";
|
|
} else {
|
|
imagesColor = "yellow";
|
|
}
|
|
}
|
|
}
|
|
|
|
// Social Tab
|
|
let socialColor: IndicatorColor = "gray";
|
|
if (metaData.openGraph && metaData.twitter) {
|
|
const og = metaData.openGraph;
|
|
const tw = metaData.twitter;
|
|
const hasOgBasics = og.title && og.description && og.image;
|
|
const hasTwBasics = tw.card && tw.title && tw.description && tw.image;
|
|
|
|
if (hasOgBasics && hasTwBasics) {
|
|
socialColor = "green";
|
|
} else if (hasOgBasics || (tw.card && tw.title)) {
|
|
socialColor = "yellow";
|
|
} else {
|
|
socialColor = "red";
|
|
}
|
|
}
|
|
|
|
// Links Tab
|
|
let linksColor: IndicatorColor = "gray";
|
|
if (metaData.links && metaData.links.length > 0) {
|
|
const hasMissingNofollow = metaData.links.some(
|
|
(link) => link.type === "external" && !link.rel.includes("nofollow")
|
|
);
|
|
linksColor = hasMissingNofollow ? "yellow" : "green";
|
|
}
|
|
|
|
// FAQ Tab
|
|
let faqColor: IndicatorColor = "gray";
|
|
if (metaData.faq && metaData.faq.length > 0) {
|
|
faqColor = "green";
|
|
}
|
|
|
|
// Schema Tab
|
|
let schemaColor: IndicatorColor = "gray";
|
|
if (metaData.schema && metaData.schema.length > 0) {
|
|
schemaColor = "green";
|
|
}
|
|
|
|
// System Tab
|
|
const systemColor: IndicatorColor =
|
|
metaData.systems && metaData.systems.length > 0 ? "green" : "gray";
|
|
|
|
// Tracking Tab
|
|
const trackingColor: IndicatorColor =
|
|
metaData.tracking && metaData.tracking.length > 0 ? "green" : "gray";
|
|
|
|
return {
|
|
analysis: analysisColor,
|
|
headlines: headlinesColor,
|
|
images: imagesColor,
|
|
social: socialColor,
|
|
links: linksColor,
|
|
faq: faqColor,
|
|
schema: schemaColor,
|
|
system: systemColor,
|
|
tracking: trackingColor,
|
|
};
|
|
}, [metaData, editableTitle, editableDescription]);
|
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
event.preventDefault();
|
|
setLoading(true);
|
|
setError(null);
|
|
setMetaData(null);
|
|
setIsEditingTitle(false);
|
|
setIsEditingDescription(false);
|
|
setImageError(false);
|
|
|
|
const result = await extractMetaData(url, keyword);
|
|
|
|
if (result.error) {
|
|
setError(result.error);
|
|
} else if (result.data) {
|
|
setMetaData(result.data as MetaData);
|
|
}
|
|
|
|
setLoading(false);
|
|
};
|
|
|
|
const handleClear = () => {
|
|
setUrl("");
|
|
setKeyword("");
|
|
setLoading(false);
|
|
setError(null);
|
|
setMetaData(null);
|
|
};
|
|
|
|
return (
|
|
<div className="w-full space-y-6">
|
|
<MetaFormInputs
|
|
url={url}
|
|
setUrl={setUrl}
|
|
keyword={keyword}
|
|
setKeyword={setKeyword}
|
|
handleSubmit={handleSubmit}
|
|
handleClear={handleClear}
|
|
loading={loading}
|
|
showClearButton={!!(url || metaData || error)}
|
|
/>
|
|
|
|
{loading && <ResultsSkeleton />}
|
|
|
|
{!loading && error && (
|
|
<Card className="border-destructive bg-destructive/10">
|
|
<CardContent className="p-4">
|
|
<p className="text-destructive text-center">{error}</p>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
{!loading && metaData && (
|
|
<Tabs defaultValue="analysis" className="w-full">
|
|
<TabsList className="mb-4">
|
|
<TabsTrigger value="analysis">
|
|
Meta Analysis
|
|
{tabColors && <TabIndicator color={tabColors.analysis} />}
|
|
</TabsTrigger>
|
|
{metaData.headlines && metaData.headlines.length > 0 && (
|
|
<TabsTrigger value="headlines">
|
|
Headlines
|
|
{tabColors && <TabIndicator color={tabColors.headlines} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.images && metaData.images.length > 0 && (
|
|
<TabsTrigger value="images">
|
|
Images
|
|
{tabColors && <TabIndicator color={tabColors.images} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.links && metaData.links.length > 0 && (
|
|
<TabsTrigger value="links">
|
|
Links
|
|
{tabColors && <TabIndicator color={tabColors.links} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.openGraph && metaData.twitter && (
|
|
<TabsTrigger value="social">
|
|
Social
|
|
{tabColors && <TabIndicator color={tabColors.social} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.schema && metaData.schema.length > 0 && (
|
|
<TabsTrigger value="schema">
|
|
Schema
|
|
{tabColors && <TabIndicator color={tabColors.schema} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.systems && metaData.systems.length > 0 && (
|
|
<TabsTrigger value="system">
|
|
System
|
|
{tabColors && <TabIndicator color={tabColors.system} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.tracking && metaData.tracking.length > 0 && (
|
|
<TabsTrigger value="tracking">
|
|
Tracking
|
|
{tabColors && <TabIndicator color={tabColors.tracking} />}
|
|
</TabsTrigger>
|
|
)}
|
|
{metaData.faq && metaData.faq.length > 0 && (
|
|
<TabsTrigger value="faq">
|
|
FAQ
|
|
{tabColors && <TabIndicator color={tabColors.faq} />}
|
|
</TabsTrigger>
|
|
)}
|
|
</TabsList>
|
|
|
|
<TabsContent value="analysis">
|
|
<AnalysisTab
|
|
metaData={metaData}
|
|
url={url}
|
|
editableTitle={editableTitle}
|
|
setEditableTitle={setEditableTitle}
|
|
isEditingTitle={isEditingTitle}
|
|
setIsEditingTitle={setIsEditingTitle}
|
|
editableDescription={editableDescription}
|
|
setEditableDescription={setEditableDescription}
|
|
isEditingDescription={isEditingDescription}
|
|
setIsEditingDescription={setIsEditingDescription}
|
|
imageError={imageError}
|
|
setImageError={setImageError}
|
|
/>
|
|
</TabsContent>
|
|
|
|
{metaData.headlines && metaData.headlines.length > 0 && (
|
|
<TabsContent value="headlines">
|
|
<Card className="w-full shadow-lg rounded-lg">
|
|
<CardContent className="p-0">
|
|
<HeadlineTree
|
|
headlines={metaData.headlines}
|
|
keyword={metaData.keyword}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.images && metaData.images.length > 0 && (
|
|
<TabsContent value="images">
|
|
<Card className="w-full shadow-lg rounded-lg">
|
|
<CardContent className="p-6">
|
|
<ImageAltDisplay
|
|
images={metaData.images}
|
|
keyword={metaData.keyword}
|
|
/>
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.links && metaData.links.length > 0 && (
|
|
<TabsContent value="links">
|
|
<Card className="w-full shadow-lg rounded-lg">
|
|
<CardContent className="p-6">
|
|
<LinksDisplay links={metaData.links} />
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.openGraph && metaData.twitter && (
|
|
<TabsContent value="social">
|
|
<SocialTab
|
|
openGraph={metaData.openGraph}
|
|
twitter={metaData.twitter}
|
|
/>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.faq && metaData.faq.length > 0 && (
|
|
<TabsContent value="faq">
|
|
<Card className="w-full shadow-lg rounded-lg">
|
|
<CardContent className="p-6">
|
|
<FaqDisplay faqs={metaData.faq} />
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.schema && metaData.schema.length > 0 && (
|
|
<TabsContent value="schema">
|
|
<Card className="w-full shadow-lg rounded-lg">
|
|
<CardContent className="p-6">
|
|
<SchemaDisplay schemas={metaData.schema} />
|
|
</CardContent>
|
|
</Card>
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.systems && metaData.systems.length > 0 && (
|
|
<TabsContent value="system">
|
|
<SystemDisplay systems={metaData.systems} />
|
|
</TabsContent>
|
|
)}
|
|
|
|
{metaData.tracking && metaData.tracking.length > 0 && (
|
|
<TabsContent value="tracking">
|
|
<TrackingDisplay tools={metaData.tracking} />
|
|
</TabsContent>
|
|
)}
|
|
</Tabs>
|
|
)}
|
|
</div>
|
|
);
|
|
} |