461 lines
24 KiB
TypeScript
461 lines
24 KiB
TypeScript
import React, { useState, useCallback, useEffect, useRef } from 'react';
|
|
import type { AppState, CompetitorCandidate, Product, TargetIndustry, Keyword, SilverBullet, Battlecard, ReferenceAnalysis } from './types';
|
|
import { fetchStep1Data, fetchStep2Data, fetchStep3Data, fetchStep4Data, fetchStep5Data_SilverBullets, fetchStep6Data_Conclusion, fetchStep7Data_Battlecards, fetchStep8Data_ReferenceAnalysis, reanalyzeCompetitor } from './services/geminiService';
|
|
import { generatePdfReport } from './services/pdfService';
|
|
import InputForm from './components/InputForm';
|
|
import StepIndicator from './components/StepIndicator';
|
|
import Step1Extraction from './components/Step1_Extraction';
|
|
import Step2Keywords from './components/Step2_Keywords';
|
|
import Step3Competitors from './components/Step3_Competitors';
|
|
import Step4Analysis from './components/Step4_Analysis';
|
|
import Step5SilverBullets from './components/Step5_SilverBullets';
|
|
import Step6Conclusion from './components/Step6_Conclusion';
|
|
import Step7_Battlecards from './components/Step7_Battlecards';
|
|
import Step8_References from './components/Step8_References';
|
|
import LoadingSpinner from './components/LoadingSpinner';
|
|
import { translations } from './translations';
|
|
|
|
const SunIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>);
|
|
const MoonIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>);
|
|
const RestartIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor" strokeWidth={2}><path strokeLinecap="round" strokeLinejoin="round" d="M4 4v5h5M4 12a9 9 0 109-9" /></svg>);
|
|
const DownloadIcon = () => (<svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>);
|
|
const ChevronDownIcon = () => (<svg className="w-5 h-5 ml-2 -mr-1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true"><path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" /></svg>);
|
|
|
|
|
|
const App: React.FC = () => {
|
|
const [appState, setAppState] = useState<AppState | null>(null);
|
|
const [isLoading, setIsLoading] = useState<boolean>(false);
|
|
const [error, setError] = useState<string | null>(null);
|
|
const [theme, setTheme] = useState<'light' | 'dark'>('dark');
|
|
const [highestStep, setHighestStep] = useState(0);
|
|
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
|
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
|
|
|
const t = translations[appState?.initial_params?.language || 'de'];
|
|
|
|
useEffect(() => {
|
|
if (theme === 'dark') {
|
|
document.documentElement.classList.add('dark');
|
|
} else {
|
|
document.documentElement.classList.remove('dark');
|
|
}
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
|
setIsDropdownOpen(false);
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", handleClickOutside);
|
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
|
}, []);
|
|
|
|
const handleRestart = () => {
|
|
setAppState(null);
|
|
setIsLoading(false);
|
|
setError(null);
|
|
setHighestStep(0);
|
|
};
|
|
|
|
const handleStartAnalysis = useCallback(async (startUrl: string, maxCompetitors: number, marketScope: string, language: 'de' | 'en') => {
|
|
setIsLoading(true);
|
|
setError(null);
|
|
setAppState(null);
|
|
setHighestStep(0);
|
|
const currentT = translations[language];
|
|
try {
|
|
const { products, target_industries } = await fetchStep1Data(startUrl, language);
|
|
setAppState({
|
|
step: 1,
|
|
initial_params: { start_url: startUrl, max_competitors: maxCompetitors, market_scope: marketScope, language },
|
|
company: { name: new URL(startUrl).hostname.replace('www.', ''), start_url: startUrl },
|
|
products: products,
|
|
target_industries: target_industries,
|
|
keywords: [],
|
|
competitor_candidates: [],
|
|
competitors_shortlist: [],
|
|
analyses: [],
|
|
silver_bullets: [],
|
|
conclusion: null,
|
|
battlecards: [],
|
|
reference_analysis: [],
|
|
reference_analysis_grounding: [],
|
|
});
|
|
setHighestStep(1);
|
|
} catch (e) {
|
|
console.error("Error in Step 1:", e);
|
|
setError(currentT.errors.step1);
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
const handleUpdateAnalysis = useCallback((index: number, updatedAnalysis: any) => {
|
|
setAppState(prevState => {
|
|
if (!prevState) return null;
|
|
const newAnalyses = [...prevState.analyses];
|
|
newAnalyses[index] = updatedAnalysis;
|
|
return { ...prevState, analyses: newAnalyses };
|
|
});
|
|
}, []);
|
|
|
|
const handleConfirmStep = useCallback(async () => {
|
|
if (!appState) return;
|
|
|
|
setIsLoading(true);
|
|
setError(null);
|
|
const nextStep = appState.step + 1;
|
|
const lang = appState.initial_params.language;
|
|
try {
|
|
let newState: Partial<AppState> = {};
|
|
switch (appState.step) {
|
|
case 1:
|
|
const { keywords } = await fetchStep2Data(appState.products, appState.target_industries, lang);
|
|
newState = { keywords, step: 2 };
|
|
break;
|
|
case 2:
|
|
const { competitor_candidates } = await fetchStep3Data(appState.keywords, appState.initial_params.market_scope, lang);
|
|
newState = { competitor_candidates, step: 3 };
|
|
break;
|
|
case 3:
|
|
const shortlist = [...appState.competitor_candidates]
|
|
.sort((a, b) => b.confidence - a.confidence)
|
|
.slice(0, appState.initial_params.max_competitors)
|
|
.map(c => ({
|
|
...c,
|
|
manual_urls: c.manual_urls ? c.manual_urls.split('\n').map(u => u.trim()).filter(u => u) : []
|
|
}));
|
|
const { analyses } = await fetchStep4Data(appState.company, shortlist, lang);
|
|
newState = { competitors_shortlist: shortlist, analyses, step: 4 };
|
|
break;
|
|
case 4:
|
|
const { silver_bullets } = await fetchStep5Data_SilverBullets(appState.company, appState.analyses, lang);
|
|
newState = { silver_bullets, step: 5 };
|
|
break;
|
|
case 5:
|
|
const { conclusion } = await fetchStep6Data_Conclusion(appState.company, appState.products, appState.target_industries, appState.analyses, appState.silver_bullets, lang);
|
|
newState = { conclusion, step: 6 };
|
|
break;
|
|
case 6:
|
|
const { battlecards } = await fetchStep7Data_Battlecards(appState.company, appState.analyses, appState.silver_bullets, lang);
|
|
newState = { battlecards, step: 7 };
|
|
break;
|
|
case 7:
|
|
const { reference_analysis, groundingMetadata } = await fetchStep8Data_ReferenceAnalysis(appState.competitors_shortlist, lang);
|
|
newState = { reference_analysis, reference_analysis_grounding: groundingMetadata, step: 8 };
|
|
break;
|
|
}
|
|
setAppState(prevState => ({ ...prevState!, ...newState }));
|
|
if (nextStep > highestStep) {
|
|
setHighestStep(nextStep);
|
|
}
|
|
} catch (e) {
|
|
console.error(`Error in Step ${appState.step + 1}:`, e);
|
|
setError(translations[lang].errors.generic(appState.step + 1));
|
|
} finally {
|
|
setIsLoading(false);
|
|
}
|
|
}, [appState, highestStep]);
|
|
|
|
const handleUpdateState = useCallback((key: keyof AppState, value: any) => {
|
|
setAppState(prevState => {
|
|
if (!prevState) return null;
|
|
return { ...prevState, [key]: value };
|
|
});
|
|
}, []);
|
|
|
|
const renderStepContent = () => {
|
|
if (!appState) return null;
|
|
switch (appState.step) {
|
|
case 1: return <Step1Extraction products={appState.products} industries={appState.target_industries} onProductsChange={(p) => handleUpdateState('products', p)} onIndustriesChange={(i) => handleUpdateState('target_industries', i)} t={t.step1} lang={appState.initial_params.language} />;
|
|
case 2: return <Step2Keywords keywords={appState.keywords} onKeywordsChange={(k) => handleUpdateState('keywords', k)} t={t.step2} />;
|
|
case 3: return <Step3Competitors candidates={appState.competitor_candidates} onCandidatesChange={(c) => handleUpdateState('competitor_candidates', c)} maxCompetitors={appState.initial_params.max_competitors} t={t.step3} />;
|
|
case 4: return <Step4Analysis analyses={appState.analyses} company={appState.company} onAnalysisUpdate={handleUpdateAnalysis} t={t.step4} />;
|
|
case 5: return <Step5SilverBullets silver_bullets={appState.silver_bullets} t={t.step5} />;
|
|
case 6: return <Step6Conclusion appState={appState} t={t.step6} />;
|
|
case 7: return <Step7_Battlecards appState={appState} t={t.step7} />;
|
|
case 8: return <Step8_References analyses={appState.reference_analysis} groundingMetadata={appState.reference_analysis_grounding} t={t.step8} />;
|
|
default: return null;
|
|
}
|
|
};
|
|
|
|
// Download logic
|
|
const handleDownloadJson = () => {
|
|
if (!appState) return;
|
|
const content = JSON.stringify(appState, null, 2);
|
|
const blob = new Blob([content], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
link.download = `analysis_${appState.company.name}.json`;
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
setIsDropdownOpen(false);
|
|
};
|
|
|
|
const transformMatrixData = (matrixData: any[] | undefined): { [row: string]: { [col: string]: string } } => {
|
|
const tableData: { [row: string]: { [col: string]: string } } = {};
|
|
if (!matrixData || !Array.isArray(matrixData) || matrixData.length === 0) {
|
|
return tableData;
|
|
}
|
|
|
|
const allCompetitors = new Set<string>();
|
|
matrixData.forEach(row => {
|
|
if (row && Array.isArray(row.availability)) {
|
|
row.availability.forEach(item => {
|
|
if (item && typeof item.competitor === 'string') {
|
|
allCompetitors.add(item.competitor);
|
|
}
|
|
});
|
|
}
|
|
});
|
|
const competitorList = Array.from(allCompetitors).sort();
|
|
|
|
matrixData.forEach(row => {
|
|
if (!row) return;
|
|
|
|
const rowKey = 'product' in row ? row.product : ('industry' in row ? row.industry : undefined);
|
|
|
|
if (typeof rowKey !== 'string' || !rowKey) {
|
|
return;
|
|
}
|
|
|
|
const availabilityMap = new Map<string, boolean>();
|
|
if (Array.isArray(row.availability)) {
|
|
row.availability.forEach(item => {
|
|
if (item && typeof item.competitor === 'string') {
|
|
availabilityMap.set(item.competitor, item.has_offering);
|
|
}
|
|
});
|
|
}
|
|
|
|
const rowObject: { [col: string]: string } = {};
|
|
competitorList.forEach(competitor => {
|
|
rowObject[competitor] = availabilityMap.get(competitor) ? '✓' : ' ';
|
|
});
|
|
tableData[rowKey] = rowObject;
|
|
});
|
|
|
|
return tableData;
|
|
};
|
|
|
|
|
|
const generateMarkdownReport = (): string => {
|
|
if (!appState) return "";
|
|
const langT = t.step6.markdown;
|
|
let md = `# ${langT.title}: ${appState.company.name}\n\n`;
|
|
md += `**${langT.startUrl}:** ${appState.initial_params.start_url}\n`;
|
|
md += `**${langT.marketScope}:** ${appState.initial_params.market_scope}\n\n`;
|
|
|
|
md += `## ${langT.step1_title}\n`;
|
|
md += `### ${langT.step1_products}\n`;
|
|
appState.products.forEach(p => md += `- **${p.name}:** ${p.purpose}\n`);
|
|
md += `\n### ${langT.step1_industries}\n`;
|
|
appState.target_industries.forEach(i => md += `- ${i.name}\n`);
|
|
|
|
md += `\n## ${langT.step4_title}\n`;
|
|
appState.analyses.forEach(a => {
|
|
md += `### ${a.competitor.name} (Overlap: ${a.overlap_score}%)\n`;
|
|
md += `- **${langT.portfolio}:** ${a.portfolio.map(p => p.product).join(', ')}\n`;
|
|
md += `- **${langT.targetIndustries}:** ${a.target_industries.join(', ')}\n`;
|
|
md += `- **${langT.differentiators}:**\n`;
|
|
a.differentiators.forEach(d => md += ` - ${d}\n`);
|
|
md += `\n`;
|
|
});
|
|
|
|
md += `## ${langT.step5_title}\n`;
|
|
appState.silver_bullets.forEach(b => {
|
|
md += `- **${langT.against} ${b.competitor_name}:** "${b.statement}"\n`;
|
|
});
|
|
|
|
if (appState.battlecards && appState.battlecards.length > 0) {
|
|
md += `\n## ${langT.step7_title}\n\n`;
|
|
appState.battlecards.forEach(card => {
|
|
md += `### ${langT.against} ${card.competitor_name}\n\n`;
|
|
md += `**${langT.profile}:**\n`;
|
|
md += `- **${langT.focus}:** ${card.competitor_profile.focus}\n`;
|
|
md += `- **${langT.positioning}:** ${card.competitor_profile.positioning}\n\n`;
|
|
|
|
md += `**${langT.strengthsVsWeaknesses}:**\n`;
|
|
(card.strengths_vs_weaknesses || []).forEach(s => md += `- ${s}\n`);
|
|
md += `\n`;
|
|
|
|
md += `**${langT.landmineQuestions}:**\n`;
|
|
(card.landmine_questions || []).forEach(q => md += `- ${q}\n`);
|
|
md += `\n`;
|
|
|
|
md += `**${langT.silverBullet}:**\n`;
|
|
md += `> "${card.silver_bullet}"\n\n`;
|
|
});
|
|
}
|
|
|
|
if (appState.reference_analysis && appState.reference_analysis.length > 0) {
|
|
md += `\n## ${langT.step8_title}\n`;
|
|
appState.reference_analysis.forEach(analysis => {
|
|
md += `### ${analysis.competitor_name}\n`;
|
|
if ((analysis.references || []).length > 0) {
|
|
(analysis.references || []).forEach(ref => {
|
|
md += `- **${ref.name}** (${ref.industry || 'N/A'})\n`;
|
|
if(ref.testimonial_snippet) md += ` - *"${ref.testimonial_snippet}"*\n`;
|
|
if(ref.case_study_url) md += ` - [${langT.caseStudyLink}](${ref.case_study_url})\n`;
|
|
});
|
|
} else {
|
|
md += ` - ${langT.noReferencesFound}\n`;
|
|
}
|
|
md += `\n`;
|
|
});
|
|
if (appState.reference_analysis_grounding && appState.reference_analysis_grounding.length > 0) {
|
|
md += `\n#### ${langT.sources}\n`;
|
|
appState.reference_analysis_grounding
|
|
.filter(chunk => chunk.web && chunk.web.uri)
|
|
.forEach(chunk => {
|
|
md += `- [${chunk.web.title || chunk.web.uri}](${chunk.web.uri})\n`;
|
|
});
|
|
}
|
|
}
|
|
|
|
if(appState.conclusion) {
|
|
md += `\n## ${langT.step6_title}\n`;
|
|
const transformForMdTable = (data: { [key: string]: { [key: string]: string } }) => {
|
|
if (Object.keys(data).length === 0) return { head: '', body: '' };
|
|
const headers = Object.keys(Object.values(data)[0] || {});
|
|
const head = `| | ${headers.join(' | ')} |\n`;
|
|
const separator = `|---|${headers.map(() => '---').join('|')}|\n`;
|
|
const body = Object.entries(data).map(([row, cols]) => `| **${row}** | ${headers.map(h => cols[h] || ' ').join(' | ')} |`).join('\n');
|
|
return { head, body: separator + body };
|
|
};
|
|
const productMatrixForTable = transformMatrixData(appState.conclusion.product_matrix);
|
|
const industryMatrixForTable = transformMatrixData(appState.conclusion.industry_matrix);
|
|
|
|
md += `### ${langT.productMatrix}\n`;
|
|
const prodMd = transformForMdTable(productMatrixForTable);
|
|
md += prodMd.head + prodMd.body + '\n\n';
|
|
|
|
md += `### ${langT.industryMatrix}\n`;
|
|
const indMd = transformForMdTable(industryMatrixForTable);
|
|
md += indMd.head + indMd.body + '\n\n';
|
|
|
|
md += `### ${langT.summary}\n${appState.conclusion.summary}\n\n`;
|
|
md += `### ${langT.opportunities}\n${appState.conclusion.opportunities}\n\n`;
|
|
md += `### ${langT.nextQuestions}\n`;
|
|
(appState.conclusion.next_questions || []).forEach(q => md += `- ${q}\n`);
|
|
}
|
|
|
|
return md;
|
|
};
|
|
|
|
const handleDownloadMd = () => {
|
|
const mdContent = generateMarkdownReport();
|
|
const blob = new Blob([mdContent], { type: 'text/markdown' });
|
|
const url = URL.createObjectURL(blob);
|
|
const link = document.createElement('a');
|
|
link.href = url;
|
|
if (appState) {
|
|
link.download = `analysis_${appState.company.name}.md`;
|
|
}
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
URL.revokeObjectURL(url);
|
|
setIsDropdownOpen(false);
|
|
};
|
|
|
|
const handleDownloadPdf = async () => {
|
|
if (!appState) return;
|
|
await generatePdfReport(appState, t.step6.markdown);
|
|
setIsDropdownOpen(false);
|
|
};
|
|
|
|
return (
|
|
<div className="min-h-screen p-4 sm:p-6 lg:p-8">
|
|
<header className="flex justify-between items-center mb-6">
|
|
<h1 className="text-3xl font-bold">{t.appTitle}</h1>
|
|
<div className="flex items-center space-x-4">
|
|
{appState && highestStep >= 6 && (
|
|
<div className="relative inline-block text-left" ref={dropdownRef}>
|
|
<div>
|
|
<button
|
|
type="button"
|
|
onClick={() => setIsDropdownOpen(!isDropdownOpen)}
|
|
className="inline-flex justify-center w-full rounded-lg border border-transparent shadow-sm px-4 py-2 bg-green-600 text-base font-medium text-white hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-light-secondary dark:focus:ring-offset-brand-secondary focus:ring-green-500"
|
|
>
|
|
<DownloadIcon />
|
|
{t.step6.downloadButton}
|
|
<ChevronDownIcon />
|
|
</button>
|
|
</div>
|
|
{isDropdownOpen && (
|
|
<div className="origin-top-right absolute right-0 mt-2 w-56 rounded-md shadow-lg bg-light-secondary dark:bg-brand-secondary ring-1 ring-black ring-opacity-5 z-20">
|
|
<div className="py-1" role="menu" aria-orientation="vertical" aria-labelledby="options-menu">
|
|
<a href="#" onClick={(e) => { e.preventDefault(); handleDownloadJson(); }} className="block px-4 py-2 text-sm text-light-text dark:text-brand-text hover:bg-light-accent dark:hover:bg-brand-accent" role="menuitem">{t.step6.downloadJson}</a>
|
|
<a href="#" onClick={(e) => { e.preventDefault(); handleDownloadMd(); }} className="block px-4 py-2 text-sm text-light-text dark:text-brand-text hover:bg-light-accent dark:hover:bg-brand-accent" role="menuitem">{t.step6.downloadMd}</a>
|
|
<a href="#" onClick={(e) => { e.preventDefault(); handleDownloadPdf(); }} className="block px-4 py-2 text-sm text-light-text dark:text-brand-text hover:bg-light-accent dark:hover:bg-brand-accent" role="menuitem">{t.step6.downloadPdf}</a>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
{appState && (
|
|
<button onClick={handleRestart} className="p-2 rounded-full hover:bg-light-accent dark:hover:bg-brand-accent" title={t.restartAnalysis}>
|
|
<RestartIcon />
|
|
</button>
|
|
)}
|
|
<button onClick={() => setTheme(theme === 'dark' ? 'light' : 'dark')} className="p-2 rounded-full hover:bg-light-accent dark:hover:bg-brand-accent">
|
|
{theme === 'dark' ? <SunIcon /> : <MoonIcon />}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<main className="mx-auto">
|
|
{!appState && !isLoading && <InputForm onStart={handleStartAnalysis} />}
|
|
|
|
{isLoading && !appState && <LoadingSpinner t={t.loadingSpinner} />}
|
|
|
|
{appState && (
|
|
<div className="grid grid-cols-1 lg:grid-cols-4 gap-8 items-start">
|
|
<aside className="lg:col-span-1 lg:sticky lg:top-8">
|
|
<div className="bg-light-secondary dark:bg-brand-secondary p-4 rounded-lg shadow-md mb-6">
|
|
<h3 className="font-bold text-lg mb-2">{t.companyCard.title}</h3>
|
|
<p className="text-sm break-words">{appState.company.name}</p>
|
|
</div>
|
|
<StepIndicator
|
|
currentStep={appState.step}
|
|
highestStep={highestStep}
|
|
onStepClick={(step) => setAppState(prevState => ({...prevState!, step}))}
|
|
t={t.stepIndicator}
|
|
/>
|
|
</aside>
|
|
|
|
<div className="lg:col-span-3">
|
|
<div className="min-h-[400px]">
|
|
{error && (
|
|
<div className="bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded-lg mb-6" role="alert">
|
|
<p className="font-bold">{t.errors.title}</p>
|
|
<p>{error}</p>
|
|
</div>
|
|
)}
|
|
{isLoading ? <LoadingSpinner t={t.loadingSpinner} /> : renderStepContent()}
|
|
</div>
|
|
|
|
{appState.step < 8 && !isLoading && (
|
|
<div className="mt-8 text-right">
|
|
<button
|
|
onClick={handleConfirmStep}
|
|
className="bg-brand-highlight hover:bg-blue-600 text-white font-bold py-3 px-6 rounded-lg shadow-lg transition duration-300 transform hover:scale-105"
|
|
>
|
|
{t.nextStepButton}
|
|
</button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</main>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default App; |