sushilideaclan01's picture
.
8ffe335
"use client";
import React, { useState, useEffect } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/Card";
import { Input } from "@/components/ui/Input";
import { Select } from "@/components/ui/Select";
import { LoadingSpinner } from "@/components/ui/LoadingSpinner";
import { getAllConcepts, getCompatibleConcepts } from "@/lib/api/endpoints";
import { useMatrixStore } from "@/store/matrixStore";
import type { ConceptInfo, ConceptsResponse } from "@/types/api";
import { InfoButton } from "@/components/ui/InfoButton";
interface ConceptSelectorProps {
onSelect?: (concept: ConceptInfo) => void;
selectedConcept?: ConceptInfo | null;
angleKey?: string | null;
}
export const ConceptSelector: React.FC<ConceptSelectorProps> = ({
onSelect,
selectedConcept,
angleKey,
}) => {
const { concepts, compatibleConcepts, isLoading, setConcepts, setCompatibleConcepts, setIsLoading } = useMatrixStore();
const [searchTerm, setSearchTerm] = useState("");
const [selectedCategory, setSelectedCategory] = useState<string>("");
const [showCompatibleOnly, setShowCompatibleOnly] = useState(false);
useEffect(() => {
if (!concepts) {
loadConcepts();
}
}, []);
useEffect(() => {
if (angleKey && showCompatibleOnly) {
loadCompatibleConcepts(angleKey);
}
}, [angleKey, showCompatibleOnly]);
const loadConcepts = async () => {
setIsLoading(true);
try {
const data = await getAllConcepts();
setConcepts(data);
} catch (error) {
console.error("Failed to load concepts:", error);
} finally {
setIsLoading(false);
}
};
const loadCompatibleConcepts = async (key: string) => {
try {
const data = await getCompatibleConcepts(key);
// Convert compatible concepts to full ConceptInfo by fetching from all concepts
if (concepts) {
const fullConcepts: ConceptInfo[] = [];
for (const compat of data.compatible_concepts) {
// Find full concept info from all concepts
let found = false;
for (const [catKey, cat] of Object.entries(concepts.categories)) {
const fullConcept = cat.concepts.find((c) => c.key === compat.key);
if (fullConcept) {
// Add category to the concept
fullConcepts.push({
...fullConcept,
category: cat.name,
});
found = true;
break;
}
}
// If not found, create a minimal version
if (!found) {
fullConcepts.push({
key: compat.key,
name: compat.name,
structure: compat.structure,
visual: "",
category: "",
});
}
}
setCompatibleConcepts(fullConcepts);
} else {
// Fallback to minimal concepts if full concepts not loaded
const minimalConcepts: ConceptInfo[] = data.compatible_concepts.map((c): ConceptInfo => ({
key: c.key,
name: c.name,
structure: c.structure,
visual: "",
category: "",
}));
setCompatibleConcepts(minimalConcepts);
}
} catch (error) {
console.error("Failed to load compatible concepts:", error);
}
};
if (isLoading && !concepts) {
return (
<Card>
<CardContent className="pt-6">
<LoadingSpinner size="lg" />
</CardContent>
</Card>
);
}
if (!concepts) {
return (
<Card>
<CardContent className="pt-6">
<p className="text-gray-500">Failed to load concepts</p>
</CardContent>
</Card>
);
}
// Filter concepts
let filteredConcepts: Array<{ category: string; concept: any }> = [];
const conceptsToUse = showCompatibleOnly && compatibleConcepts.length > 0
? compatibleConcepts
: Object.values(concepts.categories).flatMap((cat) => cat.concepts);
Object.entries(concepts.categories).forEach(([catKey, catData]) => {
if (selectedCategory && catKey !== selectedCategory) return;
catData.concepts.forEach((concept) => {
if (
(!showCompatibleOnly || compatibleConcepts.some((c) => c.key === concept.key)) &&
(!searchTerm ||
concept.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
concept.structure.toLowerCase().includes(searchTerm.toLowerCase()) ||
concept.key.toLowerCase().includes(searchTerm.toLowerCase()))
) {
filteredConcepts.push({ category: catData.name, concept });
}
});
});
const categories = Object.entries(concepts.categories).map(([key, data]) => ({
value: key,
label: data.name,
}));
return (
<Card variant="glass" className="border-2 border-transparent hover:border-cyan-200/50 transition-all duration-300">
<CardHeader>
<div className="flex items-center gap-2">
<CardTitle className="bg-gradient-to-r from-cyan-600 to-pink-600 bg-clip-text text-transparent">
Select Concept
</CardTitle>
<InfoButton
title="What is a Concept?"
content="A concept is the creative execution style or storyline you use to deliver your angle. It defines how your ad will look and feel visually.
Concepts include things like:
- Before/After comparisons
- Testimonials
- Problem/Solution narratives
- Visual metaphors
- Lifestyle imagery
Each concept has a specific structure and visual direction. When combined with an angle, they create a powerful ad that both hooks attention and drives action. Some concepts work better with certain angles - use the 'Show compatible concepts only' option to see recommended pairings."
position="bottom"
/>
</div>
{angleKey && (
<div className="mt-2">
<label className="flex items-center space-x-2">
<input
type="checkbox"
checked={showCompatibleOnly}
onChange={(e) => setShowCompatibleOnly(e.target.checked)}
className="rounded"
/>
<span className="text-sm text-gray-600">Show compatible concepts only</span>
</label>
</div>
)}
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<Input
placeholder="Search concepts..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
<Select
options={[{ value: "", label: "All Categories" }, ...categories]}
value={selectedCategory}
onChange={(e) => setSelectedCategory(e.target.value)}
/>
</div>
<div className="max-h-96 overflow-y-auto space-y-2">
{filteredConcepts.length === 0 ? (
<p className="text-center text-gray-500 py-8">No concepts found</p>
) : (
filteredConcepts.map(({ category, concept }) => (
<div
key={concept.key}
onClick={() => onSelect?.(concept)}
className={`p-3 rounded-lg border cursor-pointer transition-all duration-300 ${
selectedConcept?.key === concept.key
? "border-cyan-500 bg-gradient-to-r from-cyan-50 to-pink-50 shadow-md ring-2 ring-cyan-200"
: "border-gray-200 hover:border-cyan-300 hover:bg-gradient-to-r hover:from-gray-50 hover:to-cyan-50/30 hover:shadow-sm"
}`}
>
<div className="flex items-start justify-between">
<div className="flex-1">
<h4 className={`font-semibold transition-colors ${
selectedConcept?.key === concept.key
? "text-cyan-700"
: "text-gray-900"
}`}>
{concept.name}
</h4>
<p className={`text-sm mt-1 transition-colors ${
selectedConcept?.key === concept.key
? "text-cyan-600"
: "text-gray-600"
}`}>
{concept.structure}
</p>
<p className="text-xs text-gray-500 mt-1">{category}</p>
</div>
</div>
</div>
))
)}
</div>
</CardContent>
</Card>
);
};