Browse Source

feat: add billing page and loading animation for custom content

- Add /billing page with "Coming Soon" content and i18n support
- Add animated loading dialog with funny rotating messages when saving
  custom content (to indicate Qdrant embedding process)
- Add hideCloseButton prop to Dialog component
- Fix rapid /api/stores requests by removing duplicate fetch from
  AppSidebar (ShopContext already handles this)
- Memoize setStores in ShopContext with useCallback

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Fszontagh 4 months ago
parent
commit
b16fe85492

+ 2 - 0
shopcall.ai-main/src/App.tsx

@@ -39,6 +39,7 @@ const IntegrationsRedirect = lazy(() => import("./pages/IntegrationsRedirect"));
 const Products = lazy(() => import("./pages/Products"));
 const WebsiteContent = lazy(() => import("./pages/WebsiteContent"));
 const CustomContent = lazy(() => import("./pages/CustomContent"));
+const Billing = lazy(() => import("./pages/Billing"));
 
 // Loading component for lazy-loaded routes
 const PageLoader = () => (
@@ -79,6 +80,7 @@ const App = () => (
                 <Route path="/api-keys" element={<APIKeys />} />
                 <Route path="/onboarding" element={<Onboarding />} />
                 <Route path="/settings" element={<Settings />} />
+                <Route path="/billing" element={<Billing />} />
 
                 {/* Pages that require an active store */}
                 <Route element={<StoreProtectedRoute />}>

+ 2 - 27
shopcall.ai-main/src/components/AppSidebar.tsx

@@ -22,7 +22,6 @@ import { useTranslation } from 'react-i18next';
 import { LanguageSelector } from "./LanguageSelector";
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
 import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@/components/ui/collapsible";
-import { API_URL } from "@/lib/config";
 
 export function AppSidebar() {
   const { t } = useTranslation();
@@ -30,7 +29,7 @@ export function AppSidebar() {
   const [searchParams, setSearchParams] = useSearchParams();
   const currentPath = window.location.pathname;
   const { logout } = useAuth();
-  const { selectedShop, setSelectedShop, stores, setStores } = useShop();
+  const { selectedShop, setSelectedShop, stores } = useShop();
   // Keep AI menu open when on any AI submenu page
   const aiMenuPaths = ['/ai-config', '/products', '/website-content', '/custom-content'];
   const [isAIMenuOpen, setIsAIMenuOpen] = useState(aiMenuPaths.includes(currentPath));
@@ -40,31 +39,7 @@ export function AppSidebar() {
     setIsAIMenuOpen(aiMenuPaths.includes(currentPath));
   }, [currentPath]);
 
-  // Fetch stores on component mount
-  useEffect(() => {
-    const fetchStores = async () => {
-      try {
-        const sessionData = localStorage.getItem('session_data');
-        if (!sessionData) return;
-
-        const session = JSON.parse(sessionData);
-        const response = await fetch(`${API_URL}/api/stores`, {
-          headers: {
-            'Authorization': `Bearer ${session.session.access_token}`,
-          }
-        });
-
-        if (response.ok) {
-          const data = await response.json();
-          setStores(data.stores || []);
-        }
-      } catch (error) {
-        console.error('Error fetching stores:', error);
-      }
-    };
-
-    fetchStores();
-  }, [setStores]);
+  // Note: Stores are fetched by ShopContext on mount, no need to duplicate here
 
   const menuItems = [
     {

+ 74 - 2
shopcall.ai-main/src/components/CustomContentTextEntry.tsx

@@ -1,10 +1,14 @@
-import { useState } from "react";
+import { useState, useEffect, useRef } from "react";
 import { Button } from "@/components/ui/button";
 import { Input } from "@/components/ui/input";
 import { Label } from "@/components/ui/label";
 import { Textarea } from "@/components/ui/textarea";
 import { Alert, AlertDescription } from "@/components/ui/alert";
-import { AlertCircle } from "lucide-react";
+import {
+  Dialog,
+  DialogContent,
+} from "@/components/ui/dialog";
+import { AlertCircle, Loader2, Sparkles, Brain } from "lucide-react";
 import { useToast } from "@/hooks/use-toast";
 import { supabase } from "@/lib/supabase";
 import { useShop } from "@/components/context/ShopContext";
@@ -50,9 +54,40 @@ export function CustomContentTextEntry({ onSuccess }: CustomContentTextEntryProp
   const [saving, setSaving] = useState(false);
   const [error, setError] = useState<string | null>(null);
   const [editorHasContent, setEditorHasContent] = useState(false);
+  const [loadingMessage, setLoadingMessage] = useState("");
+  const loadingIntervalRef = useRef<NodeJS.Timeout | null>(null);
   const { toast } = useToast();
   const { selectedShop } = useShop();
 
+  // Get loading messages from translations
+  const loadingMessages = t('customContent.textEntry.savingMessages', { returnObjects: true }) as string[];
+
+  // Rotate loading messages while saving
+  useEffect(() => {
+    if (saving && Array.isArray(loadingMessages) && loadingMessages.length > 0) {
+      // Set initial message
+      setLoadingMessage(loadingMessages[Math.floor(Math.random() * loadingMessages.length)]);
+
+      // Rotate messages every 2.5 seconds
+      loadingIntervalRef.current = setInterval(() => {
+        setLoadingMessage(loadingMessages[Math.floor(Math.random() * loadingMessages.length)]);
+      }, 2500);
+    } else {
+      // Clear interval when not saving
+      if (loadingIntervalRef.current) {
+        clearInterval(loadingIntervalRef.current);
+        loadingIntervalRef.current = null;
+      }
+    }
+
+    return () => {
+      if (loadingIntervalRef.current) {
+        clearInterval(loadingIntervalRef.current);
+        loadingIntervalRef.current = null;
+      }
+    };
+  }, [saving, loadingMessages]);
+
   // Initialize TipTap editor for WYSIWYG mode
   const editor = useEditor({
     extensions: [
@@ -293,6 +328,43 @@ export function CustomContentTextEntry({ onSuccess }: CustomContentTextEntryProp
           {saving ? t('customContent.textEntry.saving') : t('customContent.textEntry.save')}
         </Button>
       </div>
+
+      {/* Loading Dialog */}
+      <Dialog open={saving} onOpenChange={() => {}}>
+        <DialogContent className="bg-slate-800 border-slate-700 sm:max-w-md" hideCloseButton>
+          <div className="flex flex-col items-center justify-center py-8 space-y-6">
+            {/* Animated Icon */}
+            <div className="relative">
+              <div className="w-20 h-20 rounded-full bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center">
+                <Brain className="w-10 h-10 text-cyan-400 animate-pulse" />
+              </div>
+              <div className="absolute -top-1 -right-1">
+                <Sparkles className="w-6 h-6 text-yellow-400 animate-bounce" />
+              </div>
+              <div className="absolute -bottom-1 -left-1">
+                <Loader2 className="w-5 h-5 text-purple-400 animate-spin" />
+              </div>
+            </div>
+
+            {/* Loading Message */}
+            <div className="text-center space-y-2">
+              <p className="text-lg font-medium text-white animate-pulse">
+                {loadingMessage}
+              </p>
+              <p className="text-sm text-slate-400">
+                {t('customContent.textEntry.saving')}
+              </p>
+            </div>
+
+            {/* Progress dots */}
+            <div className="flex space-x-2">
+              <div className="w-2 h-2 bg-cyan-400 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
+              <div className="w-2 h-2 bg-cyan-400 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
+              <div className="w-2 h-2 bg-cyan-400 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
+            </div>
+          </div>
+        </DialogContent>
+      </Dialog>
     </div>
   );
 }

+ 4 - 4
shopcall.ai-main/src/components/context/ShopContext.tsx

@@ -1,4 +1,4 @@
-import React, { createContext, useContext, useState, useEffect, ReactNode } from 'react';
+import React, { createContext, useContext, useState, useEffect, useCallback, ReactNode } from 'react';
 import { API_URL } from '@/lib/config';
 
 interface StoreData {
@@ -27,11 +27,11 @@ export const ShopProvider: React.FC<{ children: ReactNode }> = ({ children }) =>
   const [isLoading, setIsLoading] = useState(true);
   const [storesLoaded, setStoresLoaded] = useState(false);
 
-  // Wrap setStores to track when stores are actually loaded
-  const setStores = (newStores: StoreData[]) => {
+  // Wrap setStores to track when stores are actually loaded (memoized to prevent re-renders)
+  const setStores = useCallback((newStores: StoreData[]) => {
     setStoresState(newStores);
     setStoresLoaded(true);
-  };
+  }, []);
 
   // Fetch stores function - can be called from context or externally
   const fetchStores = async () => {

+ 13 - 6
shopcall.ai-main/src/components/ui/dialog.tsx

@@ -27,10 +27,15 @@ const DialogOverlay = React.forwardRef<
 ))
 DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
 
+interface DialogContentProps
+  extends React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content> {
+  hideCloseButton?: boolean
+}
+
 const DialogContent = React.forwardRef<
   React.ElementRef<typeof DialogPrimitive.Content>,
-  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
->(({ className, children, ...props }, ref) => (
+  DialogContentProps
+>(({ className, children, hideCloseButton, ...props }, ref) => (
   <DialogPortal>
     <DialogOverlay />
     <DialogPrimitive.Content
@@ -42,10 +47,12 @@ const DialogContent = React.forwardRef<
       {...props}
     >
       {children}
-      <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
-        <X className="h-4 w-4" />
-        <span className="sr-only">Close</span>
-      </DialogPrimitive.Close>
+      {!hideCloseButton && (
+        <DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
+          <X className="h-4 w-4" />
+          <span className="sr-only">Close</span>
+        </DialogPrimitive.Close>
+      )}
     </DialogPrimitive.Content>
   </DialogPortal>
 ))

+ 95 - 0
shopcall.ai-main/src/i18n/locales/de.json

@@ -1489,6 +1489,101 @@
     "playStereo": "Stereo-Aufzeichnung abspielen",
     "noRecording": "Keine Aufzeichnung verfügbar"
   },
+  "customContent": {
+    "title": "Benutzerdefinierter Inhalt",
+    "description": "Laden Sie PDFs hoch und erstellen Sie Texteinträge für die Wissensdatenbank Ihres KI-Assistenten",
+    "uploadPdf": "PDF hochladen",
+    "addTextEntry": "Texteintrag hinzufügen",
+    "tabs": {
+      "all": "Alle Inhalte",
+      "pdfs": "PDFs",
+      "textEntries": "Texteinträge"
+    },
+    "textEntry": {
+      "title": "Texteintrag hinzufügen",
+      "description": "Erstellen Sie einen benutzerdefinierten Texteintrag für die Wissensdatenbank Ihres KI-Assistenten",
+      "titleLabel": "Titel",
+      "titlePlaceholder": "Geben Sie einen Titel ein",
+      "contentLabel": "Inhalt",
+      "contentPlaceholder": "Geben Sie Ihren Inhalt hier ein...",
+      "markdownPlaceholder": "Geben Sie Ihren Inhalt im Markdown-Format ein...\n\n# Überschrift\n## Unterüberschrift\n\n**Fett** und *kursiv* Text",
+      "wysiwygPlaceholder": "Beginnen Sie mit der Eingabe oder fügen Sie formatierten Text aus Word ein...",
+      "formatPlain": "Klartext",
+      "formatMarkdown": "Markdown",
+      "formatWysiwyg": "Rich Text",
+      "validationNote": "Hinweis: URLs und Bilder sind im Inhalt nicht erlaubt",
+      "characterCount": "{{count}} / 50.000 Zeichen",
+      "save": "Speichern",
+      "saving": "Wird gespeichert...",
+      "savingMessages": [
+        "Bringen dem KI neue Tricks bei...",
+        "Füttern den Roboter mit Wissen...",
+        "Verwandeln Weisheit in Vektoren...",
+        "Streuen etwas Machine-Learning-Magie...",
+        "Machen Inhalte KI-lesbar...",
+        "Laden Gehirnnahrung für den Bot hoch...",
+        "Verwandeln Text in Roboter-Treibstoff...",
+        "Beamen Inhalte in die Cloud...",
+        "Trainieren winzige digitale Neuronen...",
+        "Betten Wissen in die Matrix ein..."
+      ],
+      "cancel": "Abbrechen",
+      "successTitle": "Eintrag erstellt",
+      "successDescription": "\"{{title}}\" wurde zur Wissensdatenbank hinzugefügt",
+      "errorTitle": "Erstellung fehlgeschlagen",
+      "errorRequired": "Bitte geben Sie Titel und Inhalt an",
+      "errorMaxLength": "Inhalt überschreitet maximale Länge von 50.000 Zeichen",
+      "errorSelectShop": "Bitte wählen Sie zuerst einen Shop aus",
+      "errorContainsUrl": "Inhalt darf keine URLs enthalten. Bitte entfernen Sie alle Links.",
+      "errorContainsImage": "Inhalt darf keine Bilder enthalten. Bitte entfernen Sie alle Bildverweise.",
+      "errorInvalidContent": "Inhalt enthält ungültige Elemente"
+    },
+    "list": {
+      "loading": "Wird geladen...",
+      "noContent": "Noch kein benutzerdefinierter Inhalt. Laden Sie ein PDF hoch oder erstellen Sie einen Texteintrag, um zu beginnen.",
+      "view": "Inhalt anzeigen",
+      "table": {
+        "type": "Typ",
+        "title": "Titel",
+        "details": "Details",
+        "status": "Status",
+        "chunks": "Chunks",
+        "created": "Erstellt",
+        "actions": "Aktionen",
+        "textEntry": "Texteintrag",
+        "pages": "Seiten"
+      }
+    },
+    "viewer": {
+      "loading": "Inhalt wird geladen...",
+      "chunks": "Chunks",
+      "pdfView": "PDF-Ansicht",
+      "textView": "Textansicht",
+      "openInNewTab": "In neuem Tab öffnen"
+    },
+    "status": {
+      "pending": "Ausstehend",
+      "processing": "Wird verarbeitet",
+      "ready": "Bereit",
+      "failed": "Fehlgeschlagen"
+    }
+  },
+  "billing": {
+    "title": "Abrechnung & Tarif",
+    "subtitle": "Verwalten Sie Ihr Abonnement und Ihre Rechnungsdetails",
+    "comingSoon": {
+      "badge": "Demnächst",
+      "title": "Abrechnungsfunktionen sind unterwegs",
+      "description": "Wir arbeiten hart daran, Ihnen ein nahtloses Abrechnungserlebnis zu bieten. Bleiben Sie dran für Abonnementverwaltung, Rechnungen und mehr.",
+      "features": {
+        "flexiblePlans": "Flexible Tarife",
+        "specialOffers": "Sonderangebote",
+        "easyPayments": "Einfache Zahlungen"
+      },
+      "notifyButton": "Benachrichtigen",
+      "note": "Wir informieren Sie, sobald die Abrechnungsfunktionen verfügbar sind."
+    }
+  },
   "crawler": {
     "title": "ShopCall.ai Web-Crawler",
     "backHome": "Zurück zur Startseite",

+ 28 - 0
shopcall.ai-main/src/i18n/locales/en.json

@@ -1688,6 +1688,18 @@
       "characterCount": "{{count}} / 50,000 characters",
       "save": "Save",
       "saving": "Saving...",
+      "savingMessages": [
+        "Teaching AI new tricks...",
+        "Feeding knowledge to the robot...",
+        "Converting wisdom into vectors...",
+        "Sprinkling some machine learning magic...",
+        "Making your content AI-readable...",
+        "Uploading brain food for the bot...",
+        "Turning text into robot fuel...",
+        "Beaming content to the cloud...",
+        "Training tiny digital neurons...",
+        "Embedding knowledge in the matrix..."
+      ],
       "cancel": "Cancel",
       "successTitle": "Entry created",
       "successDescription": "\"{{title}}\" has been added to your knowledge base",
@@ -1748,6 +1760,22 @@
       }
     }
   },
+  "billing": {
+    "title": "Billing & Plan",
+    "subtitle": "Manage your subscription and billing details",
+    "comingSoon": {
+      "badge": "Coming Soon",
+      "title": "Billing Features Are On The Way",
+      "description": "We're working hard to bring you a seamless billing experience. Stay tuned for subscription management, invoices, and more.",
+      "features": {
+        "flexiblePlans": "Flexible Plans",
+        "specialOffers": "Special Offers",
+        "easyPayments": "Easy Payments"
+      },
+      "notifyButton": "Get Notified",
+      "note": "We'll let you know as soon as billing features are available."
+    }
+  },
   "crawler": {
     "title": "ShopCall.ai Web Crawler",
     "backHome": "Back to Home",

+ 28 - 0
shopcall.ai-main/src/i18n/locales/hu.json

@@ -1638,6 +1638,18 @@
       "characterCount": "{{count}} / 50 000 karakter",
       "save": "Mentés",
       "saving": "Mentés...",
+      "savingMessages": [
+        "AI trükköket tanítunk...",
+        "Tudást etetünk a robottal...",
+        "Bölcsességet alakítunk vektorokká...",
+        "Gépi tanulás varázslatot szórunk...",
+        "AI-olvashatóvá tesszük a tartalmat...",
+        "Agyeleséget töltünk fel a botnak...",
+        "Szöveget robot üzemanyaggá alakítunk...",
+        "Tartalmat sugárzunk a felhőbe...",
+        "Apró digitális neuronokat képzünk...",
+        "Tudást ágyazunk a mátrixba..."
+      ],
       "cancel": "Mégse",
       "successTitle": "Bejegyzés létrehozva",
       "successDescription": "\"{{title}}\" hozzáadva a tudásbázishoz",
@@ -1738,6 +1750,22 @@
     "lightningFastAuth": "Villámgyors hitelesítés",
     "poweredByAI": "Fejlett AI technológiával hajtva"
   },
+  "billing": {
+    "title": "Számlázás & Csomag",
+    "subtitle": "Előfizetés és számlázási adatok kezelése",
+    "comingSoon": {
+      "badge": "Hamarosan",
+      "title": "Számlázási Funkciók Úton Vannak",
+      "description": "Keményen dolgozunk, hogy zökkenőmentes számlázási élményt nyújtsunk. Maradjon velünk az előfizetés-kezelésért, számlákért és még többért.",
+      "features": {
+        "flexiblePlans": "Rugalmas Csomagok",
+        "specialOffers": "Különleges Ajánlatok",
+        "easyPayments": "Egyszerű Fizetések"
+      },
+      "notifyButton": "Értesítést Kérek",
+      "note": "Értesítjük, amint a számlázási funkciók elérhetők lesznek."
+    }
+  },
   "crawler": {
     "title": "ShopCall.ai Web Crawler",
     "backHome": "Vissza a Főoldalra",

+ 97 - 0
shopcall.ai-main/src/pages/Billing.tsx

@@ -0,0 +1,97 @@
+import { SidebarProvider } from "@/components/ui/sidebar";
+import { AppSidebar } from "@/components/AppSidebar";
+import { Card, CardContent } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { CreditCard, Sparkles, Bell, Rocket, Zap, Gift } from "lucide-react";
+import { useTranslation } from 'react-i18next';
+
+export default function Billing() {
+  const { t } = useTranslation();
+
+  return (
+    <SidebarProvider>
+      <div className="min-h-screen flex w-full bg-slate-900">
+        <AppSidebar />
+        <main className="flex-1 bg-slate-900 text-white">
+          <div className="flex flex-col gap-6 p-6">
+            <div>
+              <h1 className="text-3xl font-bold text-white">{t('billing.title')}</h1>
+              <p className="text-slate-400 mt-2">
+                {t('billing.subtitle')}
+              </p>
+            </div>
+
+            {/* Coming Soon Card */}
+            <div className="flex items-center justify-center min-h-[60vh]">
+              <Card className="bg-gradient-to-br from-slate-800 via-slate-800 to-slate-900 border-slate-700 max-w-2xl w-full overflow-hidden relative">
+                {/* Animated background elements */}
+                <div className="absolute inset-0 overflow-hidden">
+                  <div className="absolute -top-20 -right-20 w-40 h-40 bg-cyan-500/10 rounded-full blur-3xl animate-pulse" />
+                  <div className="absolute -bottom-20 -left-20 w-40 h-40 bg-purple-500/10 rounded-full blur-3xl animate-pulse delay-1000" />
+                </div>
+
+                <CardContent className="p-12 text-center relative z-10">
+                  {/* Icon */}
+                  <div className="mb-8 relative inline-block">
+                    <div className="w-24 h-24 rounded-full bg-gradient-to-br from-cyan-500/20 to-purple-500/20 flex items-center justify-center mx-auto">
+                      <CreditCard className="w-12 h-12 text-cyan-400" />
+                    </div>
+                    <div className="absolute -top-1 -right-1">
+                      <Sparkles className="w-6 h-6 text-yellow-400 animate-pulse" />
+                    </div>
+                  </div>
+
+                  {/* Coming Soon Badge */}
+                  <div className="inline-flex items-center gap-2 bg-cyan-500/10 border border-cyan-500/30 rounded-full px-4 py-2 mb-6">
+                    <Rocket className="w-4 h-4 text-cyan-400" />
+                    <span className="text-cyan-400 font-medium text-sm">{t('billing.comingSoon.badge')}</span>
+                  </div>
+
+                  {/* Title */}
+                  <h2 className="text-3xl font-bold text-white mb-4">
+                    {t('billing.comingSoon.title')}
+                  </h2>
+
+                  {/* Description */}
+                  <p className="text-slate-400 text-lg mb-8 max-w-md mx-auto">
+                    {t('billing.comingSoon.description')}
+                  </p>
+
+                  {/* Features Preview */}
+                  <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
+                    <div className="bg-slate-700/30 rounded-lg p-4 border border-slate-600/50">
+                      <Zap className="w-6 h-6 text-yellow-400 mx-auto mb-2" />
+                      <p className="text-slate-300 text-sm font-medium">{t('billing.comingSoon.features.flexiblePlans')}</p>
+                    </div>
+                    <div className="bg-slate-700/30 rounded-lg p-4 border border-slate-600/50">
+                      <Gift className="w-6 h-6 text-pink-400 mx-auto mb-2" />
+                      <p className="text-slate-300 text-sm font-medium">{t('billing.comingSoon.features.specialOffers')}</p>
+                    </div>
+                    <div className="bg-slate-700/30 rounded-lg p-4 border border-slate-600/50">
+                      <CreditCard className="w-6 h-6 text-green-400 mx-auto mb-2" />
+                      <p className="text-slate-300 text-sm font-medium">{t('billing.comingSoon.features.easyPayments')}</p>
+                    </div>
+                  </div>
+
+                  {/* Notify Button */}
+                  <Button
+                    className="bg-cyan-500 hover:bg-cyan-600 text-white px-8 py-6 text-lg"
+                    disabled
+                  >
+                    <Bell className="w-5 h-5 mr-2" />
+                    {t('billing.comingSoon.notifyButton')}
+                  </Button>
+
+                  {/* Note */}
+                  <p className="text-slate-500 text-sm mt-6">
+                    {t('billing.comingSoon.note')}
+                  </p>
+                </CardContent>
+              </Card>
+            </div>
+          </div>
+        </main>
+      </div>
+    </SidebarProvider>
+  );
+}