|
@@ -20,7 +20,7 @@ import {
|
|
|
AlertDialogHeader,
|
|
AlertDialogHeader,
|
|
|
AlertDialogTitle,
|
|
AlertDialogTitle,
|
|
|
} from "@/components/ui/alert-dialog";
|
|
} from "@/components/ui/alert-dialog";
|
|
|
-import { Loader2, Search, Package, ChevronLeft, ChevronRight, Tag } from "lucide-react";
|
|
|
|
|
|
|
+import { Loader2, Search, Package, ChevronLeft, ChevronRight, Tag, Globe, ExternalLink, Plus, Trash2, Clock, CheckCircle, AlertCircle, Settings } from "lucide-react";
|
|
|
import { API_URL } from "@/lib/config";
|
|
import { API_URL } from "@/lib/config";
|
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
import { useTranslation } from "react-i18next";
|
|
import { useTranslation } from "react-i18next";
|
|
@@ -60,6 +60,39 @@ interface DataResponse {
|
|
|
disabled_count: number;
|
|
disabled_count: number;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+interface ScraperContent {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ content_type: 'shipping' | 'contacts' | 'terms' | 'faq';
|
|
|
|
|
+ title?: string;
|
|
|
|
|
+ content: string;
|
|
|
|
|
+ scraped_at: string;
|
|
|
|
|
+ metadata?: Record<string, any>;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ScraperCustomUrl {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ content_type: 'shipping' | 'contacts' | 'terms' | 'faq';
|
|
|
|
|
+ enabled: boolean;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+ last_scraped_at?: string;
|
|
|
|
|
+ status?: 'pending' | 'completed' | 'failed';
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+interface ScraperShopStatus {
|
|
|
|
|
+ id: string;
|
|
|
|
|
+ url: string;
|
|
|
|
|
+ status: 'active' | 'inactive';
|
|
|
|
|
+ last_scraped_at?: string;
|
|
|
|
|
+ next_scheduled_scrape?: string;
|
|
|
|
|
+ scheduled_enabled: boolean;
|
|
|
|
|
+ total_urls_found: number;
|
|
|
|
|
+ total_content_items: number;
|
|
|
|
|
+ created_at: string;
|
|
|
|
|
+ updated_at: string;
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
export function ManageStoreDataContent() {
|
|
export function ManageStoreDataContent() {
|
|
|
const [searchParams] = useSearchParams();
|
|
const [searchParams] = useSearchParams();
|
|
|
const { toast } = useToast();
|
|
const { toast } = useToast();
|
|
@@ -68,7 +101,7 @@ export function ManageStoreDataContent() {
|
|
|
const [stores, setStores] = useState<StoreData[]>([]);
|
|
const [stores, setStores] = useState<StoreData[]>([]);
|
|
|
const [selectedStore, setSelectedStore] = useState<StoreData | null>(null);
|
|
const [selectedStore, setSelectedStore] = useState<StoreData | null>(null);
|
|
|
const [loading, setLoading] = useState(true);
|
|
const [loading, setLoading] = useState(true);
|
|
|
- const [activeTab, setActiveTab] = useState<"products">("products");
|
|
|
|
|
|
|
+ const [activeTab, setActiveTab] = useState<"products" | "website">("products");
|
|
|
|
|
|
|
|
// Filter and search state
|
|
// Filter and search state
|
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
const [searchQuery, setSearchQuery] = useState("");
|
|
@@ -107,6 +140,15 @@ export function ManageStoreDataContent() {
|
|
|
action: () => { },
|
|
action: () => { },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Website content state
|
|
|
|
|
+ const [scraperStatus, setScraperStatus] = useState<ScraperShopStatus | null>(null);
|
|
|
|
|
+ const [scraperContent, setScraperContent] = useState<ScraperContent[]>([]);
|
|
|
|
|
+ const [customUrls, setCustomUrls] = useState<ScraperCustomUrl[]>([]);
|
|
|
|
|
+ const [websiteLoading, setWebsiteLoading] = useState(false);
|
|
|
|
|
+ const [contentTypeFilter, setContentTypeFilter] = useState<string>("all");
|
|
|
|
|
+ const [newCustomUrl, setNewCustomUrl] = useState("");
|
|
|
|
|
+ const [newCustomUrlType, setNewCustomUrlType] = useState<'shipping' | 'contacts' | 'terms' | 'faq'>('faq');
|
|
|
|
|
+
|
|
|
// Fetch stores on mount
|
|
// Fetch stores on mount
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
const fetchStores = async () => {
|
|
const fetchStores = async () => {
|
|
@@ -171,9 +213,36 @@ export function ManageStoreDataContent() {
|
|
|
// Fetch data when tab, store, or filters change
|
|
// Fetch data when tab, store, or filters change
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
if (selectedStore) {
|
|
if (selectedStore) {
|
|
|
- fetchData();
|
|
|
|
|
|
|
+ if (activeTab === "products") {
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } else if (activeTab === "website") {
|
|
|
|
|
+ fetchWebsiteData();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [selectedStore, activeTab, page, pageSize, searchQuery, statusFilter, categoryFilter, contentTypeFilter]);
|
|
|
|
|
+
|
|
|
|
|
+ // Fetch website data when website tab is active
|
|
|
|
|
+ const fetchWebsiteData = async () => {
|
|
|
|
|
+ if (!selectedStore) return;
|
|
|
|
|
+
|
|
|
|
|
+ setWebsiteLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ await Promise.all([
|
|
|
|
|
+ fetchScraperStatus(),
|
|
|
|
|
+ fetchScraperContent(),
|
|
|
|
|
+ fetchCustomUrls(),
|
|
|
|
|
+ ]);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching website data:', error);
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: "Failed to load website content data",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setWebsiteLoading(false);
|
|
|
}
|
|
}
|
|
|
- }, [selectedStore, activeTab, page, pageSize, searchQuery, statusFilter, categoryFilter]);
|
|
|
|
|
|
|
+ };
|
|
|
|
|
|
|
|
const fetchCategories = async () => {
|
|
const fetchCategories = async () => {
|
|
|
if (!selectedStore) return;
|
|
if (!selectedStore) return;
|
|
@@ -271,6 +340,170 @@ export function ManageStoreDataContent() {
|
|
|
}
|
|
}
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
|
|
+ const fetchScraperStatus = async () => {
|
|
|
|
|
+ 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}/functions/v1/scraper-management/shop-status?store_id=${selectedStore.id}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ setScraperStatus(result.shop_data || null);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching scraper status:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchScraperContent = async () => {
|
|
|
|
|
+ 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 params = new URLSearchParams({
|
|
|
|
|
+ store_id: selectedStore.id,
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (contentTypeFilter !== 'all') {
|
|
|
|
|
+ params.append('content_type', contentTypeFilter);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (searchQuery) {
|
|
|
|
|
+ params.append('search', searchQuery);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const response = await fetch(`${API_URL}/functions/v1/scraper-management/shop-content?${params.toString()}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ setScraperContent(result.content || []);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching scraper content:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchCustomUrls = async () => {
|
|
|
|
|
+ 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}/functions/v1/scraper-management/custom-urls?store_id=${selectedStore.id}`, {
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ const result = await response.json();
|
|
|
|
|
+ setCustomUrls(result.custom_urls || []);
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Error fetching custom URLs:', error);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleAddCustomUrl = async () => {
|
|
|
|
|
+ if (!selectedStore || !newCustomUrl) 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}/functions/v1/scraper-management/custom-urls`, {
|
|
|
|
|
+ method: 'POST',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ store_id: selectedStore.id,
|
|
|
|
|
+ url: newCustomUrl,
|
|
|
|
|
+ content_type: newCustomUrlType,
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Success",
|
|
|
|
|
+ description: "Custom URL added successfully",
|
|
|
|
|
+ });
|
|
|
|
|
+ setNewCustomUrl("");
|
|
|
|
|
+ fetchCustomUrls();
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const error = await response.json();
|
|
|
|
|
+ throw new Error(error.error || 'Failed to add custom URL');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: error.message || "Failed to add custom URL",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleToggleScheduling = async (enabled: 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);
|
|
|
|
|
+ const response = await fetch(`${API_URL}/functions/v1/scraper-management/scheduling`, {
|
|
|
|
|
+ method: 'PATCH',
|
|
|
|
|
+ headers: {
|
|
|
|
|
+ 'Authorization': `Bearer ${session.session.access_token}`,
|
|
|
|
|
+ 'Content-Type': 'application/json'
|
|
|
|
|
+ },
|
|
|
|
|
+ body: JSON.stringify({
|
|
|
|
|
+ store_id: selectedStore.id,
|
|
|
|
|
+ enabled,
|
|
|
|
|
+ })
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (response.ok) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Success",
|
|
|
|
|
+ description: `Scheduled scraping ${enabled ? 'enabled' : 'disabled'}`,
|
|
|
|
|
+ });
|
|
|
|
|
+ fetchScraperStatus(); // Refresh status
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const error = await response.json();
|
|
|
|
|
+ throw new Error(error.error || 'Failed to update scheduling');
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ toast({
|
|
|
|
|
+ title: "Error",
|
|
|
|
|
+ description: error.message || "Failed to update scheduling",
|
|
|
|
|
+ variant: "destructive"
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const handleToggleItem = async (itemId: string, currentEnabled: boolean, product?: Product) => {
|
|
const handleToggleItem = async (itemId: string, currentEnabled: boolean, product?: Product) => {
|
|
|
if (!selectedStore) return;
|
|
if (!selectedStore) return;
|
|
|
|
|
|
|
@@ -671,6 +904,10 @@ export function ManageStoreDataContent() {
|
|
|
<Package className="w-4 h-4 mr-2" />
|
|
<Package className="w-4 h-4 mr-2" />
|
|
|
{t('manageStoreData.tabs.products')} ({enabledCount}/{totalCount})
|
|
{t('manageStoreData.tabs.products')} ({enabledCount}/{totalCount})
|
|
|
</TabsTrigger>
|
|
</TabsTrigger>
|
|
|
|
|
+ <TabsTrigger value="website" className="data-[state=active]:bg-slate-600 data-[state=active]:text-white">
|
|
|
|
|
+ <Globe className="w-4 h-4 mr-2" />
|
|
|
|
|
+ Website Content
|
|
|
|
|
+ </TabsTrigger>
|
|
|
</TabsList>
|
|
</TabsList>
|
|
|
|
|
|
|
|
{/* Search and Filter Bar */}
|
|
{/* Search and Filter Bar */}
|
|
@@ -842,6 +1079,189 @@ export function ManageStoreDataContent() {
|
|
|
</div>
|
|
</div>
|
|
|
)}
|
|
)}
|
|
|
</TabsContent>
|
|
</TabsContent>
|
|
|
|
|
+
|
|
|
|
|
+ <TabsContent value="website">
|
|
|
|
|
+ {websiteLoading ? (
|
|
|
|
|
+ <div className="flex justify-center py-8">
|
|
|
|
|
+ <Loader2 className="w-6 h-6 text-cyan-500 animate-spin" />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <div className="space-y-6">
|
|
|
|
|
+ {/* Scraper Status Section */}
|
|
|
|
|
+ {scraperStatus && (
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white flex items-center gap-2">
|
|
|
|
|
+ <Settings className="w-5 h-5" />
|
|
|
|
|
+ Scraper Status
|
|
|
|
|
+ </CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
|
|
+ <div className="text-slate-400 text-sm">Status</div>
|
|
|
|
|
+ <div className="text-white font-medium flex items-center gap-1">
|
|
|
|
|
+ {scraperStatus.status === 'active' ? (
|
|
|
|
|
+ <CheckCircle className="w-4 h-4 text-green-500" />
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <AlertCircle className="w-4 h-4 text-red-500" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ {scraperStatus.status}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
|
|
+ <div className="text-slate-400 text-sm">URLs Found</div>
|
|
|
|
|
+ <div className="text-white font-medium">{scraperStatus.total_urls_found}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
|
|
+ <div className="text-slate-400 text-sm">Content Items</div>
|
|
|
|
|
+ <div className="text-white font-medium">{scraperStatus.total_content_items}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="bg-slate-700/50 rounded-lg p-3">
|
|
|
|
|
+ <div className="text-slate-400 text-sm">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 ? 'Enabled' : 'Disabled'}
|
|
|
|
|
+ </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()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ )}
|
|
|
|
|
+
|
|
|
|
|
+ {/* Content Type Filter */}
|
|
|
|
|
+ <div className="flex gap-4 mb-6">
|
|
|
|
|
+ <div className="flex-1 relative">
|
|
|
|
|
+ <Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-slate-400" />
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="Search website content..."
|
|
|
|
|
+ value={searchQuery}
|
|
|
|
|
+ onChange={(e) => setSearchQuery(e.target.value)}
|
|
|
|
|
+ className="pl-10 bg-slate-700 border-slate-600 text-white"
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Select value={contentTypeFilter} onValueChange={setContentTypeFilter}>
|
|
|
|
|
+ <SelectTrigger className="w-[180px] bg-slate-700 border-slate-600 text-white">
|
|
|
|
|
+ <SelectValue />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600 text-white">
|
|
|
|
|
+ <SelectItem value="all">All Content</SelectItem>
|
|
|
|
|
+ <SelectItem value="faq">FAQ</SelectItem>
|
|
|
|
|
+ <SelectItem value="terms">Terms & Conditions</SelectItem>
|
|
|
|
|
+ <SelectItem value="shipping">Shipping Info</SelectItem>
|
|
|
|
|
+ <SelectItem value="contacts">Contact Info</SelectItem>
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Custom URLs Section */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white">Custom URLs</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ <div className="flex gap-2 mb-4">
|
|
|
|
|
+ <Input
|
|
|
|
|
+ placeholder="Enter custom URL to scrape..."
|
|
|
|
|
+ value={newCustomUrl}
|
|
|
|
|
+ onChange={(e) => setNewCustomUrl(e.target.value)}
|
|
|
|
|
+ className="flex-1 bg-slate-700 border-slate-600 text-white"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Select value={newCustomUrlType} onValueChange={(value: any) => setNewCustomUrlType(value)}>
|
|
|
|
|
+ <SelectTrigger className="w-[140px] bg-slate-700 border-slate-600 text-white">
|
|
|
|
|
+ <SelectValue />
|
|
|
|
|
+ </SelectTrigger>
|
|
|
|
|
+ <SelectContent className="bg-slate-700 border-slate-600">
|
|
|
|
|
+ <SelectItem value="faq">FAQ</SelectItem>
|
|
|
|
|
+ <SelectItem value="terms">Terms</SelectItem>
|
|
|
|
|
+ <SelectItem value="shipping">Shipping</SelectItem>
|
|
|
|
|
+ <SelectItem value="contacts">Contacts</SelectItem>
|
|
|
|
|
+ </SelectContent>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ onClick={handleAddCustomUrl}
|
|
|
|
|
+ className="bg-cyan-500 hover:bg-cyan-600 text-white"
|
|
|
|
|
+ disabled={!newCustomUrl}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Plus className="w-4 h-4 mr-2" />
|
|
|
|
|
+ Add
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ {customUrls.length > 0 && (
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
|
|
+ {customUrls.map((customUrl) => (
|
|
|
|
|
+ <div key={customUrl.id} className="flex items-center gap-3 p-3 bg-slate-700/50 rounded-lg">
|
|
|
|
|
+ <div className="flex-1">
|
|
|
|
|
+ <div className="text-white text-sm flex items-center gap-2">
|
|
|
|
|
+ <ExternalLink className="w-4 h-4" />
|
|
|
|
|
+ {customUrl.url}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-slate-400 text-xs">
|
|
|
|
|
+ Type: {customUrl.content_type} | Status: {customUrl.status || 'pending'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Badge variant={customUrl.enabled ? "default" : "secondary"}>
|
|
|
|
|
+ {customUrl.enabled ? 'Enabled' : 'Disabled'}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+
|
|
|
|
|
+ {/* Scraped Content */}
|
|
|
|
|
+ <Card className="bg-slate-800 border-slate-700">
|
|
|
|
|
+ <CardHeader>
|
|
|
|
|
+ <CardTitle className="text-white">Scraped Content ({scraperContent.length})</CardTitle>
|
|
|
|
|
+ </CardHeader>
|
|
|
|
|
+ <CardContent>
|
|
|
|
|
+ {scraperContent.length === 0 ? (
|
|
|
|
|
+ <div className="text-center py-8 text-slate-400">
|
|
|
|
|
+ No content found. The scraper will discover content automatically.
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ <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 || 'Untitled'}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="text-cyan-400 text-sm break-all">{content.url}</div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Badge variant="outline" className="text-xs">
|
|
|
|
|
+ {content.content_type}
|
|
|
|
|
+ </Badge>
|
|
|
|
|
+ </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" />
|
|
|
|
|
+ Scraped: {new Date(content.scraped_at).toLocaleString()}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </CardContent>
|
|
|
|
|
+ </Card>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </TabsContent>
|
|
|
</Tabs>
|
|
</Tabs>
|
|
|
|
|
|
|
|
{/* Pagination */}
|
|
{/* Pagination */}
|