|
@@ -83,12 +83,22 @@ interface ScraperCustomUrl {
|
|
|
interface ScraperShopStatus {
|
|
interface ScraperShopStatus {
|
|
|
id: string;
|
|
id: string;
|
|
|
url: string;
|
|
url: string;
|
|
|
|
|
+ custom_id?: string;
|
|
|
status: 'active' | 'inactive';
|
|
status: 'active' | 'inactive';
|
|
|
last_scraped_at?: string;
|
|
last_scraped_at?: string;
|
|
|
next_scheduled_scrape?: string;
|
|
next_scheduled_scrape?: string;
|
|
|
scheduled_enabled: boolean;
|
|
scheduled_enabled: boolean;
|
|
|
total_urls_found: number;
|
|
total_urls_found: number;
|
|
|
total_content_items: number;
|
|
total_content_items: number;
|
|
|
|
|
+ scrape_count?: number;
|
|
|
|
|
+ last_error?: string;
|
|
|
|
|
+ last_error_at?: string;
|
|
|
|
|
+ analytics?: {
|
|
|
|
|
+ shipping_count: number;
|
|
|
|
|
+ contacts_count: number;
|
|
|
|
|
+ terms_count: number;
|
|
|
|
|
+ faq_count: number;
|
|
|
|
|
+ };
|
|
|
created_at: string;
|
|
created_at: string;
|
|
|
updated_at: string;
|
|
updated_at: string;
|
|
|
}
|
|
}
|
|
@@ -427,6 +437,30 @@ export function ManageStoreDataContent() {
|
|
|
const handleAddCustomUrl = async () => {
|
|
const handleAddCustomUrl = async () => {
|
|
|
if (!selectedStore || !newCustomUrl) return;
|
|
if (!selectedStore || !newCustomUrl) return;
|
|
|
|
|
|
|
|
|
|
+ // Validate same domain
|
|
|
|
|
+ if (selectedStore.store_url) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const storeDomain = new URL(selectedStore.store_url).hostname.toLowerCase();
|
|
|
|
|
+ const customDomain = new URL(newCustomUrl).hostname.toLowerCase();
|
|
|
|
|
+
|
|
|
|
|
+ if (customDomain !== storeDomain && !customDomain.endsWith(`.${storeDomain}`)) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.error'),
|
|
|
|
|
+ description: t('manageStoreData.website.toast.differentDomainError') || 'Custom URL must be from the same domain as your store',
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (urlError) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.error'),
|
|
|
|
|
+ description: t('manageStoreData.website.toast.invalidUrlError') || 'Invalid URL format',
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
const sessionData = localStorage.getItem('session_data');
|
|
const sessionData = localStorage.getItem('session_data');
|
|
|
if (!sessionData) throw new Error('No session data found');
|
|
if (!sessionData) throw new Error('No session data found');
|
|
@@ -447,8 +481,8 @@ export function ManageStoreDataContent() {
|
|
|
|
|
|
|
|
if (response.ok) {
|
|
if (response.ok) {
|
|
|
toast({
|
|
toast({
|
|
|
- title: t('manageStoreData.website.toast.urlAddSuccess'),
|
|
|
|
|
- description: t('manageStoreData.website.toast.urlAddSuccess'),
|
|
|
|
|
|
|
+ title: t('common.success'),
|
|
|
|
|
+ description: t('manageStoreData.website.toast.urlAddSuccess') || 'Custom URL added successfully',
|
|
|
});
|
|
});
|
|
|
setNewCustomUrl("");
|
|
setNewCustomUrl("");
|
|
|
fetchCustomUrls();
|
|
fetchCustomUrls();
|
|
@@ -465,6 +499,85 @@ export function ManageStoreDataContent() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const handleToggleCustomUrl = async (customUrlId: string, currentEnabled: boolean) => {
|
|
|
|
|
+ if (!selectedStore) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) throw new Error('No session data found');
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+
|
|
|
|
|
+ // Update the scraper-management endpoint to support this
|
|
|
|
|
+ const response = await fetch(`${API_URL}/scraper-management/custom-urls/${customUrlId}/toggle`, {
|
|
|
|
|
+ method: 'PATCH',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ store_id: selectedStore.id,
|
|
|
|
|
+ enabled: !currentEnabled,
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.success'),
|
|
|
|
|
+ description: !currentEnabled ? t('manageStoreData.website.toast.urlEnabled') || 'URL enabled' : t('manageStoreData.website.toast.urlDisabled') || 'URL disabled',
|
|
|
|
|
+ });
|
|
|
|
|
+ fetchCustomUrls();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const error = await response.json();
|
|
|
|
|
+ throw new Error(error.error || 'Failed to update custom URL');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.error'),
|
|
|
|
|
+ description: error.message || t('manageStoreData.website.toast.urlToggleError') || 'Failed to update URL',
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleDeleteCustomUrl = async (customUrlId: string) => {
|
|
|
|
|
+ if (!selectedStore) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const sessionData = localStorage.getItem('session_data');
|
|
|
|
|
+ if (!sessionData) throw new Error('No session data found');
|
|
|
|
|
+
|
|
|
|
|
+ const session = JSON.parse(sessionData);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/scraper-management/custom-urls/${customUrlId}`, {
|
|
|
|
|
+ method: 'DELETE',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ store_id: selectedStore.id,
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.success'),
|
|
|
|
|
+ description: t('manageStoreData.website.toast.urlDeleted') || 'Custom URL deleted successfully',
|
|
|
|
|
+ });
|
|
|
|
|
+ fetchCustomUrls();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const error = await response.json();
|
|
|
|
|
+ throw new Error(error.error || 'Failed to delete custom URL');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: t('common.error'),
|
|
|
|
|
+ description: error.message || t('manageStoreData.website.toast.urlDeleteError') || 'Failed to delete URL',
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleToggleScheduling = async (enabled: boolean) => {
|
|
const handleToggleScheduling = async (enabled: boolean) => {
|
|
|
if (!selectedStore) return;
|
|
if (!selectedStore) return;
|
|
|
|
|
|
|
@@ -1098,45 +1211,112 @@ export function ManageStoreDataContent() {
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|
<CardTitle className="text-white flex items-center gap-2">
|
|
|
<Settings className="w-5 h-5" />
|
|
<Settings className="w-5 h-5" />
|
|
|
- {t('manageStoreData.website.scraperStatus.title')}
|
|
|
|
|
|
|
+ {t('manageStoreData.website.scraperStatus.title') || 'Scraper Status'}
|
|
|
</CardTitle>
|
|
</CardTitle>
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
- <CardContent>
|
|
|
|
|
|
|
+ <CardContent className="space-y-4">
|
|
|
|
|
+ {/* Main Statistics Grid */}
|
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
<div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.status')}</div>
|
|
<div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.status')}</div>
|
|
|
- <div className="text-white font-medium flex items-center gap-1">
|
|
|
|
|
|
|
+ <div className="text-white font-medium flex items-center gap-1 mt-1">
|
|
|
{scraperStatus.status === 'active' ? (
|
|
{scraperStatus.status === 'active' ? (
|
|
|
- <CheckCircle className="w-4 h-4 text-green-500" />
|
|
|
|
|
|
|
+ <><CheckCircle className="w-4 h-4 text-green-500" /> Active</>
|
|
|
) : (
|
|
) : (
|
|
|
- <AlertCircle className="w-4 h-4 text-red-500" />
|
|
|
|
|
|
|
+ <><AlertCircle className="w-4 h-4 text-yellow-500" /> Inactive</>
|
|
|
)}
|
|
)}
|
|
|
- {scraperStatus.status}
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
- <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.urlsFound')}</div>
|
|
|
|
|
- <div className="text-white font-medium">{scraperStatus.total_urls_found}</div>
|
|
|
|
|
|
|
+ <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.urlsFound') || 'URLs Found'}</div>
|
|
|
|
|
+ <div className="text-white font-medium text-xl mt-1">{scraperStatus.total_urls_found}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
- <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.contentItems')}</div>
|
|
|
|
|
- <div className="text-white font-medium">{scraperStatus.total_content_items}</div>
|
|
|
|
|
|
|
+ <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.contentItems') || 'Content Sections'}</div>
|
|
|
|
|
+ <div className="text-white font-medium text-xl mt-1">{scraperStatus.total_content_items}</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
<div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
- <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.scheduled')}</div>
|
|
|
|
|
- <div className="text-white font-medium flex items-center gap-2">
|
|
|
|
|
- <Switch
|
|
|
|
|
- checked={scraperStatus.scheduled_enabled}
|
|
|
|
|
- onCheckedChange={handleToggleScheduling}
|
|
|
|
|
- className="data-[state=checked]:bg-cyan-500"
|
|
|
|
|
- />
|
|
|
|
|
- {scraperStatus.scheduled_enabled ? t('manageStoreData.website.scraperStatus.enabled') : t('manageStoreData.website.scraperStatus.disabled')}
|
|
|
|
|
|
|
+ <div className="text-slate-400 text-sm">{t('manageStoreData.website.contentViewer.statusLabels.totalScrapes') || 'Total Scrapes'}</div>
|
|
|
|
|
+ <div className="text-white font-medium text-xl mt-1">{scraperStatus.scrape_count || 0}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Content Type Breakdown */}
|
|
|
|
|
+ {scraperStatus.analytics && (
|
|
|
|
|
+ <div className="border border-slate-700 rounded-lg p-4">
|
|
|
|
|
+ <div className="text-white font-medium mb-3">{t('manageStoreData.website.contentViewer.statusLabels.contentBreakdown') || 'Content Breakdown'}</div>
|
|
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
|
|
|
|
+ <div className="flex items-center justify-between p-2 bg-slate-700/30 rounded">
|
|
|
|
|
+ <span className="text-slate-400 text-sm">FAQ</span>
|
|
|
|
|
+ <Badge variant="secondary">{scraperStatus.analytics.faq_count}</Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center justify-between p-2 bg-slate-700/30 rounded">
|
|
|
|
|
+ <span className="text-slate-400 text-sm">Shipping</span>
|
|
|
|
|
+ <Badge variant="secondary">{scraperStatus.analytics.shipping_count}</Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center justify-between p-2 bg-slate-700/30 rounded">
|
|
|
|
|
+ <span className="text-slate-400 text-sm">Terms</span>
|
|
|
|
|
+ <Badge variant="secondary">{scraperStatus.analytics.terms_count}</Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center justify-between p-2 bg-slate-700/30 rounded">
|
|
|
|
|
+ <span className="text-slate-400 text-sm">Contacts</span>
|
|
|
|
|
+ <Badge variant="secondary">{scraperStatus.analytics.contacts_count}</Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Scheduling Control */}
|
|
|
|
|
+ <div className="border border-slate-700 rounded-lg p-4">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div className="text-white font-medium">{t('manageStoreData.website.contentViewer.statusLabels.scheduledScraping') || 'Scheduled Scraping'}</div>
|
|
|
|
|
+ <div className="text-slate-400 text-sm mt-1">
|
|
|
|
|
+ {scraperStatus.scheduled_enabled
|
|
|
|
|
+ ? (t('manageStoreData.website.scraperStatus.scheduledDescription') || 'Automatically scrapes your website for new content')
|
|
|
|
|
+ : (t('manageStoreData.website.scraperStatus.scheduledDisabledDescription') || 'Enable to automatically collect content')
|
|
|
|
|
+ }
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
+ <Switch
|
|
|
|
|
+ checked={scraperStatus.scheduled_enabled}
|
|
|
|
|
+ onCheckedChange={handleToggleScheduling}
|
|
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- {scraperStatus.last_scraped_at && (
|
|
|
|
|
- <div className="mt-4 text-sm text-slate-400">
|
|
|
|
|
- Last scraped: {new Date(scraperStatus.last_scraped_at).toLocaleString()}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* Timestamps and Error Display */}
|
|
|
|
|
+ <div className="flex flex-wrap gap-4 text-sm">
|
|
|
|
|
+ {scraperStatus.last_scraped_at && (
|
|
|
|
|
+ <div className="text-slate-400">
|
|
|
|
|
+ <Clock className="w-4 h-4 inline mr-1" />
|
|
|
|
|
+ Last scraped: <span className="text-white">{new Date(scraperStatus.last_scraped_at).toLocaleString()}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {scraperStatus.next_scheduled_scrape && scraperStatus.scheduled_enabled && (
|
|
|
|
|
+ <div className="text-slate-400">
|
|
|
|
|
+ <Clock className="w-4 h-4 inline mr-1" />
|
|
|
|
|
+ Next scrape: <span className="text-white">{new Date(scraperStatus.next_scheduled_scrape).toLocaleString()}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Error Display */}
|
|
|
|
|
+ {scraperStatus.last_error && (
|
|
|
|
|
+ <div className="border border-red-800 bg-red-900/20 rounded-lg p-4">
|
|
|
|
|
+ <div className="flex items-start gap-2">
|
|
|
|
|
+ <AlertCircle className="w-5 h-5 text-red-400 flex-shrink-0 mt-0.5" />
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <div className="text-red-300 font-medium mb-1">{t('manageStoreData.website.scraperStatus.lastError') || 'Last Error'}</div>
|
|
|
|
|
+ <div className="text-red-200 text-sm">{scraperStatus.last_error}</div>
|
|
|
|
|
+ {scraperStatus.last_error_at && (
|
|
|
|
|
+ <div className="text-red-400 text-xs mt-2">
|
|
|
|
|
+ {new Date(scraperStatus.last_error_at).toLocaleString()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</CardContent>
|
|
</CardContent>
|
|
@@ -1209,15 +1389,50 @@ export function ManageStoreDataContent() {
|
|
|
<div className="flex-1">
|
|
<div className="flex-1">
|
|
|
<div className="text-white text-sm flex items-center gap-2">
|
|
<div className="text-white text-sm flex items-center gap-2">
|
|
|
<ExternalLink className="w-4 h-4" />
|
|
<ExternalLink className="w-4 h-4" />
|
|
|
- {customUrl.url}
|
|
|
|
|
|
|
+ <a href={customUrl.url} target="_blank" rel="noopener noreferrer" className="hover:text-cyan-400 underline">
|
|
|
|
|
+ {customUrl.url}
|
|
|
|
|
+ </a>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="text-slate-400 text-xs">
|
|
|
|
|
- {t('manageStoreData.website.customUrls.contentType')}: {customUrl.content_type} | Status: {customUrl.status || 'pending'}
|
|
|
|
|
|
|
+ <div className="text-slate-400 text-xs flex items-center gap-2 mt-1">
|
|
|
|
|
+ <Badge variant="outline" className="text-xs">
|
|
|
|
|
+ {customUrl.content_type}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ {customUrl.status && (
|
|
|
|
|
+ <Badge variant={
|
|
|
|
|
+ customUrl.status === 'completed' ? 'default' :
|
|
|
|
|
+ customUrl.status === 'failed' ? 'destructive' : 'secondary'
|
|
|
|
|
+ } className="text-xs">
|
|
|
|
|
+ {customUrl.status}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {customUrl.last_scraped_at && (
|
|
|
|
|
+ <span className="text-xs">
|
|
|
|
|
+ <Clock className="w-3 h-3 inline mr-1" />
|
|
|
|
|
+ {new Date(customUrl.last_scraped_at).toLocaleDateString()}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <Badge variant={customUrl.enabled ? "default" : "secondary"}>
|
|
|
|
|
- {customUrl.enabled ? t('manageStoreData.website.scraperStatus.enabled') : t('manageStoreData.website.scraperStatus.disabled')}
|
|
|
|
|
- </Badge>
|
|
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Switch
|
|
|
|
|
+ checked={customUrl.enabled}
|
|
|
|
|
+ onCheckedChange={() => handleToggleCustomUrl(customUrl.id, customUrl.enabled)}
|
|
|
|
|
+ className="data-[state=checked]:bg-cyan-500"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="sm"
|
|
|
|
|
+ variant="ghost"
|
|
|
|
|
+ onClick={() => setConfirmDialog({
|
|
|
|
|
+ open: true,
|
|
|
|
|
+ title: t('manageStoreData.website.customUrls.deleteTitle') || 'Delete Custom URL',
|
|
|
|
|
+ description: t('manageStoreData.website.customUrls.deleteDescription') || 'Are you sure you want to delete this custom URL? This action cannot be undone.',
|
|
|
|
|
+ action: () => handleDeleteCustomUrl(customUrl.id)
|
|
|
|
|
+ })}
|
|
|
|
|
+ className="text-red-400 hover:text-red-300 hover:bg-red-900/20"
|
|
|
|
|
+ >
|
|
|
|
|
+ <Trash2 className="w-4 h-4" />
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|
|
|
</div>
|
|
</div>
|
|
@@ -1228,35 +1443,115 @@ export function ManageStoreDataContent() {
|
|
|
{/* Scraped Content */}
|
|
{/* Scraped Content */}
|
|
|
<Card className="bg-slate-800 border-slate-700">
|
|
<Card className="bg-slate-800 border-slate-700">
|
|
|
<CardHeader>
|
|
<CardHeader>
|
|
|
- <CardTitle className="text-white">{t('manageStoreData.website.contentViewer.title')} ({scraperContent.length})</CardTitle>
|
|
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <CardTitle className="text-white">{t('manageStoreData.website.contentViewer.title')} ({scraperContent.length})</CardTitle>
|
|
|
|
|
+ {scraperContent.length > 0 && (
|
|
|
|
|
+ <div className="text-sm text-slate-400">
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.groupedByPage') || 'Grouped by page'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
</CardHeader>
|
|
</CardHeader>
|
|
|
<CardContent>
|
|
<CardContent>
|
|
|
{scraperContent.length === 0 ? (
|
|
{scraperContent.length === 0 ? (
|
|
|
<div className="text-center py-8 text-slate-400">
|
|
<div className="text-center py-8 text-slate-400">
|
|
|
- {t('manageStoreData.website.contentViewer.noContentDescription')}
|
|
|
|
|
|
|
+ {scraperStatus?.status === 'active' ? (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <Loader2 className="w-8 h-8 mx-auto mb-3 animate-spin text-cyan-500" />
|
|
|
|
|
+ <p>{t('manageStoreData.website.contentViewer.scrapingInProgress') || 'Scraping in progress...'}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <p>{t('manageStoreData.website.contentViewer.noContentDescription')}</p>
|
|
|
|
|
+ {scraperStatus && !scraperStatus.scheduled_enabled && (
|
|
|
|
|
+ <p className="mt-2 text-sm">
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.enableScheduling') || 'Enable scheduled scraping above to automatically collect content.'}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
) : (
|
|
) : (
|
|
|
<div className="space-y-4">
|
|
<div className="space-y-4">
|
|
|
- {scraperContent.map((content) => (
|
|
|
|
|
- <div key={content.id} className="border border-slate-600 rounded-lg p-4 bg-slate-700/30">
|
|
|
|
|
- <div className="flex items-start justify-between mb-2">
|
|
|
|
|
- <div className="flex-1">
|
|
|
|
|
- <div className="text-white font-medium flex items-center gap-2">
|
|
|
|
|
- <ExternalLink className="w-4 h-4" />
|
|
|
|
|
- {content.title || t('manageStoreData.website.contentViewer.untitled')}
|
|
|
|
|
|
|
+ {/* Group content by URL (page) */}
|
|
|
|
|
+ {Object.entries(
|
|
|
|
|
+ scraperContent.reduce((acc, content) => {
|
|
|
|
|
+ if (!acc[content.url]) {
|
|
|
|
|
+ acc[content.url] = [];
|
|
|
|
|
+ }
|
|
|
|
|
+ acc[content.url].push(content);
|
|
|
|
|
+ return acc;
|
|
|
|
|
+ }, {} as Record<string, typeof scraperContent>)
|
|
|
|
|
+ ).map(([url, contents]) => (
|
|
|
|
|
+ <div key={url} className="border border-slate-600 rounded-lg overflow-hidden">
|
|
|
|
|
+ {/* Page Header */}
|
|
|
|
|
+ <div className="bg-slate-700/50 p-3 border-b border-slate-600">
|
|
|
|
|
+ <div className="flex items-center justify-between">
|
|
|
|
|
+ <div className="flex-1 min-w-0">
|
|
|
|
|
+ <div className="flex items-center gap-2">
|
|
|
|
|
+ <Globe className="w-4 h-4 text-cyan-400 flex-shrink-0" />
|
|
|
|
|
+ <a
|
|
|
|
|
+ href={url}
|
|
|
|
|
+ target="_blank"
|
|
|
|
|
+ rel="noopener noreferrer"
|
|
|
|
|
+ className="text-white hover:text-cyan-400 underline truncate text-sm"
|
|
|
|
|
+ >
|
|
|
|
|
+ {url}
|
|
|
|
|
+ </a>
|
|
|
|
|
+ <ExternalLink className="w-3 h-3 text-slate-400 flex-shrink-0" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-slate-400 text-xs mt-1">
|
|
|
|
|
+ {contents.length} {contents.length === 1 ? 'section' : 'sections'} found
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="text-cyan-400 text-sm break-all">{content.url}</div>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- <Badge variant="outline" className="text-xs">
|
|
|
|
|
- {content.content_type}
|
|
|
|
|
- </Badge>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="text-slate-300 text-sm line-clamp-3 mb-2">
|
|
|
|
|
- {content.content.substring(0, 200)}...
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="text-slate-400 text-xs flex items-center gap-1">
|
|
|
|
|
- <Clock className="w-3 h-3" />
|
|
|
|
|
- {t('manageStoreData.website.contentViewer.scraped')}: {new Date(content.scraped_at).toLocaleString()}
|
|
|
|
|
|
|
+
|
|
|
|
|
+ {/* Content Sections */}
|
|
|
|
|
+ <div className="divide-y divide-slate-600">
|
|
|
|
|
+ {contents.map((content) => (
|
|
|
|
|
+ <div key={content.id} className="p-4 bg-slate-800/50 hover:bg-slate-700/30 transition-colors">
|
|
|
|
|
+ <div className="flex items-start justify-between mb-2">
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <div className="text-white font-medium">
|
|
|
|
|
+ {content.title || t('manageStoreData.website.contentViewer.untitled')}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Badge
|
|
|
|
|
+ variant="outline"
|
|
|
|
|
+ className="text-xs ml-2"
|
|
|
|
|
+ >
|
|
|
|
|
+ {content.content_type}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-slate-300 text-sm mb-3 whitespace-pre-wrap">
|
|
|
|
|
+ {content.content.length > 500 ? (
|
|
|
|
|
+ <details className="cursor-pointer">
|
|
|
|
|
+ <summary className="hover:text-white">
|
|
|
|
|
+ {content.content.substring(0, 500)}...
|
|
|
|
|
+ <span className="text-cyan-400 ml-2">{t('manageStoreData.website.contentViewer.readMore') || 'Read more'}</span>
|
|
|
|
|
+ </summary>
|
|
|
|
|
+ <div className="mt-2 pt-2 border-t border-slate-600">
|
|
|
|
|
+ {content.content}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </details>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ content.content
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="flex items-center gap-4 text-slate-400 text-xs">
|
|
|
|
|
+ <div className="flex items-center gap-1">
|
|
|
|
|
+ <Clock className="w-3 h-3" />
|
|
|
|
|
+ {t('manageStoreData.website.contentViewer.scraped')}: {new Date(content.scraped_at).toLocaleString()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {content.metadata && Object.keys(content.metadata).length > 0 && (
|
|
|
|
|
+ <Badge variant="secondary" className="text-xs">
|
|
|
|
|
+ {Object.keys(content.metadata).length} metadata fields
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
))}
|
|
))}
|