מדריך פיתוח Frontend

מדריך פיתוח Frontend

מסמך זה מספק תיעוד טכני מקיף לפיתוח frontend בתוסף WooAI Chatbot Pro ל-WordPress. הוא מכסה את מערכת הבנייה, ארכיטקטורת הקומפוננטות, תבניות ניהול מצב, ותהליכי פיתוח.


1. מחסנית טכנולוגית

ה-frontend בנוי עם טכנולוגיות מודרניות של אקוסיסטם React המותאמות לאינטגרציה עם WordPress:

טכנולוגיה גרסה מטרה
React 18.3.1 ספריית קומפוננטות UI
TypeScript 5.7.2 בדיקת טיפוסים סטטית
Tailwind CSS 3.4.15 עיצוב מבוסס utility
Radix UI 1.x רכיבים נגישים בסיסיים
class-variance-authority 0.7.1 וריאנטים של קומפוננטות
Recharts 2.15.4 ויזואליזציות אנליטיקה
React Hook Form 7.55.0 ניהול מצב טפסים
Framer Motion 11.16.1 אנימציות
Lucide React 0.487.0 ספריית אייקונים
dnd-kit 6.3.1 פונקציונליות גרור ושחרר

החלטות ארכיטקטוניות מרכזיות

  • ללא Redux/Zustand: המצב מנוהל דרך React Context לפשטות ותאימות עם WordPress
  • ללא React Router: הניווט מנוהל דרך כתובות תפריט הניהול של WordPress עם סנכרון מצב
  • React מאוגד: React מאוגד (לא חיצוני) כדי להבטיח עקביות גרסאות בסביבות WordPress שונות
  • בידוד CSS: הווידג’ט משתמש בקידומת wac- למניעת התנגשויות עיצוב עם ערכות נושא של WordPress

למה אנחנו מאגדים React במקום להשתמש ב-wp.element:
WordPress 6.4 מגיע עם React 18.2, אבל חלק מהאחסונים עדיין מריצים 6.1 עם React 17.
קיבלנו 3 דיווחי באגים מהתנגשויות עם ערכות נושא לפני שוויתרנו ואגדנו אותו.
כן, זה מוסיף ~40KB. שווה את השקט הנפשי.

דברים שיתפסו אתכם

  1. Hot reload לא עובד עם WordPress – פשוט קבלו את זה, הריצו npm run dev ורעננו ידנית
  2. אל תשתמשו ב-window.fetch – השתמשו בעטיפת apiClient שלנו, היא מטפלת ב-nonces וניסיונות חוזרים
  3. מחלקות Tailwind נמחקות – אם המחלקה הדינמית שלכם לא עובדת, בדקו את safelist ב-tailwind.config.js
  4. React DevTools לא יציגו שמות קומפוננטות בפרודקשן – זה מכוון (bundle קטן יותר)

2. מבנה הפרויקט

assets/src/
├── admin/                    # פאנל ניהול (wp-admin)
│   ├── index.tsx            # נקודת כניסה
│   ├── styles.css           # סגנונות ספציפיים לאדמין
│   ├── types.ts             # הגדרות טיפוסים לאדמין
│   ├── components/          # קומפוננטות ספציפיות לאדמין
│   │   ├── AdminLayout.tsx  # עטיפת layout ראשית
│   │   ├── AdminSidebar.tsx # סרגל ניווט צדדי
│   │   ├── PlaybookEditor/  # קומפוננטות עורך מורכבות
│   │   │   ├── StepEditor.tsx
│   │   │   ├── StepList.tsx
│   │   │   └── StepTypes/   # עורכי סוגי צעדים
│   │   └── ProductSelector/ # קומפוננטת בחירת מוצר
│   └── pages/               # קומפוננטות עמודי אדמין
│       ├── Dashboard.tsx
│       ├── AIProviders.tsx
│       ├── Analytics.tsx
│       ├── Appearance.tsx
│       ├── Languages.tsx
│       ├── Playbooks.tsx
│       ├── Promotions.tsx
│       ├── RAG.tsx
│       └── Topics.tsx
│
├── chat/                     # ווידג'ט צ'אט ב-frontend
│   ├── index.tsx            # נקודת כניסה
│   ├── styles.css           # סגנונות הווידג'ט
│   ├── components/          # קומפוננטות הווידג'ט
│   │   ├── ChatWidgetBottom.tsx  # מכולת הווידג'ט הראשית
│   │   ├── ChatMessage.tsx       # בועת הודעה
│   │   ├── ProductCarousel.tsx   # תצוגת מוצרים
│   │   ├── TypingIndicator.tsx   # מצב טעינה
│   │   ├── TopicsBar.tsx         # הצעות נושאים
│   │   ├── PlaybookStep.tsx      # UI של Playbook
│   │   └── PlaybookSteps/        # מרנדרים של צעדים
│   └── utils/
│       └── wacClassNames.ts # מקדם מחלקות הווידג'ט
│
├── components/ui/            # קומפוננטות shadcn/ui משותפות
│   ├── button.tsx
│   ├── card.tsx
│   ├── dialog.tsx
│   ├── form.tsx
│   ├── input.tsx
│   ├── select.tsx
│   ├── tabs.tsx
│   └── ... (40+ קומפוננטות)
│
├── contexts/                 # ספקי React Context
│   ├── ChatContext.tsx      # ניהול מצב הצ'אט
│   ├── SettingsContext.tsx  # הגדרות התוסף
│   ├── AnalyticsContext.tsx # נתוני אנליטיקה
│   └── PlaybookContext.tsx  # הרצת Playbook
│
├── services/                 # שכבת שירותי API
│   ├── chatService.ts       # פעולות API של צ'אט
│   ├── settingsService.ts   # API הגדרות
│   ├── analyticsService.ts  # API אנליטיקה
│   ├── cartService.ts       # עגלת WooCommerce
│   └── productsService.ts   # חיפוש מוצרים
│
├── types/                    # הגדרות TypeScript
│   ├── api.ts               # טיפוסי בקשה/תגובה של API
│   ├── models.ts            # מודלים של הדומיין
│   ├── settings.ts          # ממשקי הגדרות
│   ├── playbook.ts          # טיפוסי Playbook
│   └── promotion.ts         # טיפוסי מבצעים
│
├── utils/                    # כלי עזר משותפים
│   ├── api/
│   │   ├── client.ts        # לקוח HTTP עם ניסיון חוזר
│   │   ├── errors.ts        # טיפול בשגיאות
│   │   └── index.ts
│   ├── hooks/
│   │   ├── useDebounce.ts
│   │   ├── useAsync.ts
│   │   └── index.ts
│   ├── cn.ts                # מאחד שמות מחלקות
│   └── logger.ts            # כלי לוגינג
│
├── hooks/                    # הוקס React מותאמים
│   └── useTranslation.ts    # הוק i18n
│
└── analytics/                # מעקב אנליטיקה
    └── index.ts             # מעקב אירועים

3. מערכת הבנייה

תצורת Webpack

הבנייה משתמשת ב-Webpack 5 עם שתי נקודות כניסה כפולות ל-bundles של אדמין וווידג’ט צ’אט:

// webpack.config.js (מפושט)
module.exports = (env, argv) => ({
  entry: {
    admin: './assets/src/admin/index.tsx',
    'chat-widget': './assets/src/chat/index.tsx',
  },

  output: {
    path: path.resolve(__dirname, 'assets'),
    filename: (pathData) => {
      if (pathData.chunk.name === 'admin') {
        return 'admin/js/admin.bundle.js';
      }
      if (pathData.chunk.name === 'chat-widget') {
        return 'chat/js/chat-widget.bundle.js';
      }
      return '[name].bundle.js';
    },
  },

  resolve: {
    extensions: ['.ts', '.tsx', '.js', '.jsx', '.json'],
    alias: {
      '@': path.resolve(__dirname, 'assets/src'),
      '@admin': path.resolve(__dirname, 'assets/src/admin'),
      '@chat': path.resolve(__dirname, 'assets/src/chat'),
      '@components': path.resolve(__dirname, 'assets/src/components'),
      '@hooks': path.resolve(__dirname, 'assets/src/hooks'),
      '@utils': path.resolve(__dirname, 'assets/src/utils'),
      '@types': path.resolve(__dirname, 'assets/src/types'),
    },
  },

  externals: {
    jquery: 'jQuery',
    '@wordpress/i18n': 'wp.i18n',
  },
});

מבנה הפלט

assets/
├── admin/
│   ├── js/
│   │   └── admin.bundle.js       # אפליקציית React לאדמין
│   └── css/
│       └── admin.bundle.css      # סגנונות אדמין
├── chat/
│   ├── js/
│   │   └── chat-widget.bundle.js # אפליקציית React לווידג'ט
│   └── css/
│       └── chat-widget.bundle.css # סגנונות הווידג'ט
└── images/
    └── ai-assistant-figma.png    # אייקון הווידג'ט

אסטרטגיית פיצול Chunks

קוד ספקים מפוצל לחלקים נפרדים לשמירה אופטימלית במטמון:

optimization: {
  splitChunks: {
    cacheGroups: {
      adminVendor: {
        test: /[/]node_modules[/]/,
        name: 'admin-vendor',
        chunks: (chunk) => chunk.name === 'admin',
        priority: 10,
      },
      chatVendor: {
        test: /[/]node_modules[/]/,
        name: 'chat-vendor',
        chunks: (chunk) => chunk.name === 'chat-widget',
        priority: 10,
      },
    },
  },
},

סקריפטים של npm

# פיתוח עם מצב watch
npm run dev

# בניית פרודקשן (ממוזער, ללא console logs)
npm run build

# בניית פיתוח (ללא מיזעור)
npm run build:dev

# בדיקת טיפוסים בלבד
npm run typecheck

# בדיקת קוד עם תיקון אוטומטי
npm run lint

# הרצת בדיקות
npm run test

# ניקוי artifacts של בנייה
npm run clean

בניות פיתוח לעומת פרודקשן

תכונה פיתוח פרודקשן
Source maps eval-source-map source-map
מיזעור מבוטל TerserPlugin
Console logs נשמרים מוסרים
בדיקת טיפוסים transpileOnly בדיקת טיפוסים מלאה
חילוץ CSS style-loader MiniCssExtractPlugin

4. ארכיטקטורת קומפוננטות

היררכיית פאנל האדמין

AdminApp
├── SettingsProvider (context)
│   └── AnalyticsProvider (context)
│       └── AdminLayout
│           ├── AdminSidebar
│           └── [Page Component]
│               ├── Dashboard
│               ├── AIProviders
│               ├── Analytics
│               ├── Appearance
│               ├── Languages
│               ├── Playbooks
│               │   └── PlaybookEditor
│               │       ├── StepList
│               │       └── StepEditor
│               │           └── [StepType]Editor
│               ├── Promotions
│               ├── RAG
│               └── Topics

היררכיית קומפוננטות הווידג’ט

ChatWidget
└── ChatProvider (context)
    └── ChatWidgetBottom
        ├── [icon state]     → כפתור צף
        ├── [collapsed state] → שורת קלט בלבד
        ├── [minimized state] → חלון קומפקטי
        └── [expanded state]  → פאנל צ'אט מלא
            ├── Header
            ├── MessageList
            │   ├── ChatMessage
            │   │   └── ProductCarousel
            │   ├── TypingIndicator
            │   └── PlaybookStep
            ├── TopicsBar
            └── InputArea

תבנית קומפוננטה: shadcn/ui עם CVA

קומפוננטות משתמשות ב-class-variance-authority לניהול וריאנטים:

// assets/src/components/ui/button.tsx
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "./utils";

const buttonVariants = cva(
  // סגנונות בסיס
  "inline-flex items-center justify-center rounded-md text-sm font-medium transition-all",
  {
    variants: {
      variant: {
        default: "bg-primary text-primary-foreground hover:bg-primary/90",
        destructive: "bg-destructive text-white hover:bg-destructive/90",
        outline: "border bg-background hover:bg-accent",
        secondary: "bg-secondary text-secondary-foreground",
        ghost: "hover:bg-accent hover:text-accent-foreground",
        link: "text-primary underline-offset-4 hover:underline",
      },
      size: {
        default: "h-9 px-4 py-2",
        sm: "h-8 rounded-md px-3",
        lg: "h-10 rounded-md px-6",
        icon: "size-9",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

function Button({
  className,
  variant,
  size,
  asChild = false,
  ...props
}: React.ComponentProps<"button"> & VariantProps<typeof buttonVariants> & {
  asChild?: boolean;
}) {
  const Comp = asChild ? Slot : "button";
  return (
    <Comp
      className={cn(buttonVariants({ variant, size, className }))}
      {...props}
    />
  );
}

תבנית קומפוזיציה של Radix UI

רכיבי Radix בסיסיים עטופים עם exports מעוצבים:

// assets/src/components/ui/dialog.tsx
import * as DialogPrimitive from "@radix-ui/react-dialog";

const Dialog = DialogPrimitive.Root;
const DialogTrigger = DialogPrimitive.Trigger;

const DialogContent = React.forwardRef<
  React.ElementRef<typeof DialogPrimitive.Content>,
  React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
  <DialogPortal>
    <DialogOverlay />
    <DialogPrimitive.Content
      ref={ref}
      className={cn(
        "fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%]",
        "bg-background p-6 shadow-lg duration-200",
        className
      )}
      {...props}
    >
      {children}
    </DialogPrimitive.Content>
  </DialogPortal>
));

5. ניהול מצב

ארכיטקטורת Context

התוסף משתמש בארבעה React Contexts עיקריים:

┌─────────────────────────────────────────────────────────────┐
│                     React App Root                          │
├─────────────────────────────────────────────────────────────┤
│  ┌───────────────────────────────────────────────────────┐  │
│  │                 SettingsProvider                      │  │
│  │  - הגדרות התוסף (ספק AI, מראה וכו')                  │  │
│  │  - עדכונים אופטימיסטיים עם rollback                  │  │
│  ├───────────────────────────────────────────────────────┤  │
│  │  ┌─────────────────────────────────────────────────┐  │  │
│  │  │              AnalyticsProvider                  │  │  │
│  │  │  - מדדי KPI, נתוני funnel                       │  │  │
│  │  │  - רשימת שיחות                                  │  │  │
│  │  │  - פונקציונליות ייצוא CSV                       │  │  │
│  │  ├─────────────────────────────────────────────────┤  │  │
│  │  │  ┌───────────────────────────────────────────┐  │  │  │
│  │  │  │            ChatProvider                   │  │  │  │
│  │  │  │  - מערך הודעות                            │  │  │  │
│  │  │  │  - ניהול סשן                              │  │  │  │
│  │  │  │  - handlers לשליחה/קבלה של הודעות         │  │  │  │
│  │  │  ├───────────────────────────────────────────┤  │  │  │
│  │  │  │  ┌─────────────────────────────────────┐  │  │  │  │
│  │  │  │  │        PlaybookProvider             │  │  │  │  │
│  │  │  │  │  - מצב playbook פעיל                │  │  │  │  │
│  │  │  │  │  - ניווט צעדים                       │  │  │  │  │
│  │  │  │  │  - משתנים שנאספו                     │  │  │  │  │
│  │  │  │  └─────────────────────────────────────┘  │  │  │  │
│  │  │  └───────────────────────────────────────────┘  │  │  │
│  │  └─────────────────────────────────────────────────┘  │  │
│  └───────────────────────────────────────────────────────┘  │
└─────────────────────────────────────────────────────────────┘

מימוש ChatContext

// assets/src/contexts/ChatContext.tsx
export interface ChatContextValue {
  messages: ChatMessage[];
  sessionId: string | null;
  loading: boolean;
  error: ApiError | null;
  sendMessage: (message: string, context?: ChatContext) => Promise<void>;
  clearHistory: () => Promise<void>;
  loadHistory: () => Promise<void>;
  getRelatedProducts: (query?: string, limit?: number) => Promise<Product[] | undefined>;
}

export const ChatProvider: React.FC<ChatProviderProps> = ({
  children,
  autoInit = true,
  initialContext
}) => {
  const [messages, setMessages] = useState<ChatMessage[]>([]);
  const [sessionId, setSessionId] = useState<string | null>(null);
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<ApiError | null>(null);

  // אתחול סשן בעת mount
  useEffect(() => {
    if (autoInit) {
      initializeSession();
    }
  }, [autoInit, initializeSession]);

  const sendMessage = useCallback(async (message: string, context?: ChatContext) => {
    if (!message.trim()) return;

    setLoading(true);
    setError(null);

    // הוספת הודעת המשתמש מיידית (אופטימיסטית)
    const userMessage: ChatMessage = {
      id: `user-${Date.now()}`,
      role: 'user',
      content: message,
      timestamp: Date.now(),
    };
    setMessages((prev) => [...prev, userMessage]);

    try {
      const response = await chatService.sendMessage(message, sessionId, context);

      // הוספת תגובת העוזר
      const assistantMessage: ChatMessage = {
        id: `assistant-${Date.now()}`,
        role: 'assistant',
        content: response.message,
        timestamp: Date.now(),
        products: response.products,
        suggestions: response.suggestions,
      };
      setMessages((prev) => [...prev, assistantMessage]);
    } catch (err) {
      // הסרת הודעת המשתמש האופטימיסטית בשגיאה
      setMessages((prev) => prev.slice(0, -1));
      setError(err as ApiError);
    } finally {
      setLoading(false);
    }
  }, [sessionId]);

  // ... מתודות נוספות

  return (
    <ChatContext.Provider value={{ messages, sessionId, loading, error, sendMessage, ... }}>
      {children}
    </ChatContext.Provider>
  );
};

// Hook עם בדיקת בטיחות
export const useChat = (): ChatContextValue => {
  const context = useContext(ChatContext);
  if (context === undefined) {
    throw new Error('useChat must be used within a ChatProvider');
  }
  return context;
};

SettingsContext עם עדכונים אופטימיסטיים

// assets/src/contexts/SettingsContext.tsx
const updateSettings = useCallback(async (newSettings: Partial<Settings>) => {
  if (!settings) throw new Error('Settings not loaded');

  // שמירה ל-rollback
  const previousSettings = settings;

  // עדכון אופטימיסטי
  setSettings((prev) => ({
    ...prev,
    ...newSettings,
    aiProvider: { ...prev.aiProvider, ...(newSettings.aiProvider || {}) },
    appearance: { ...prev.appearance, ...(newSettings.appearance || {}) },
  }));

  try {
    const updatedSettings = await settingsService.updateSettings(newSettings);
    setSettings(updatedSettings);
  } catch (err) {
    // Rollback בשגיאה
    setSettings(previousSettings);
    throw err;
  }
}, [settings]);

מכונת מצבים של PlaybookContext

// assets/src/contexts/PlaybookContext.tsx
export interface PlaybookState {
  active: boolean;
  playbookId: string | null;
  stateId: number | null;
  currentStepIndex: number;
  totalSteps: number;
  stepData: PlaybookStepData | null;
  status: 'idle' | 'loading' | 'active' | 'paused' | 'completed';
  collectedVariables: Record<string, unknown>;
}

// מעברי מצב:
// idle -> loading (startPlaybook)
// loading -> active (הצלחת API)
// active -> loading (submitResponse)
// active -> paused (pausePlaybook)
// paused -> active (resumePlaybook)
// active -> completed (playbook הסתיים)
// כל מצב -> idle (cancelPlaybook)

6. שירותי API

ארכיטקטורת לקוח HTTP

לקוח ה-API מספק תכונות ברמה ארגונית:

// assets/src/utils/api/client.ts
class ApiClient {
  private baseUrl: string;
  private nonce: string;
  private requestInterceptors: RequestInterceptor[] = [];
  private responseInterceptors: ResponseInterceptor[] = [];
  private errorInterceptors: ErrorInterceptor[] = [];
  private defaultRetryCount: number = 3;
  private defaultRetryDelay: number = 1000;

  // זיהוי אוטומטי של כתובת WordPress API
  private detectApiUrl(): string {
    const globals = window.wooAiChatbot ?? window.wooAIChatbot;
    if (globals?.apiUrl) return globals.apiUrl;
    return `${window.location.origin}/wp-json/woo-ai/v1/`;
  }

  // לוגיקת ניסיון חוזר עם exponential backoff
  private async request<T>(endpoint: string, config: RequestConfig = {}): Promise<T> {
    const { retry = true, retryCount = 0, maxRetries = 3 } = config;

    try {
      const response = await fetch(url, {
        ...config,
        headers: {
          'Content-Type': 'application/json',
          'X-WP-Nonce': this.nonce,
          ...config.headers,
        },
      });

      if (!response.ok) {
        const error = new ApiError(message, response.status, code);

        // ניסיון חוזר בשגיאות ניתנות לניסיון
        if (retry && error.isRetryable() && retryCount < maxRetries) {
          await this.sleep(this.calculateBackoff(retryCount + 1, 1000));
          return this.request<T>(endpoint, { ...config, retryCount: retryCount + 1 });
        }
        throw error;
      }

      return response.json();
    } catch (error) {
      // לוגיקת ניסיון חוזר לשגיאות רשת
      if (retry && retryCount < maxRetries) {
        await this.sleep(this.calculateBackoff(retryCount + 1, 1000));
        return this.request<T>(endpoint, { ...config, retryCount: retryCount + 1 });
      }
      throw error;
    }
  }

  // מתודות נוחות
  get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;
  post<T>(endpoint: string, data?: unknown): Promise<T>;
  put<T>(endpoint: string, data?: unknown): Promise<T>;
  delete<T>(endpoint: string): Promise<T>;
}

מחלקת ApiError מותאמת

// assets/src/utils/api/client.ts
export class ApiError extends Error {
  constructor(
    message: string,
    public statusCode?: number,
    public code?: string,
    public data?: unknown
  ) {
    super(message);
    this.name = 'ApiError';
  }

  isNetworkError(): boolean {
    return !this.statusCode || this.statusCode === 0;
  }

  isClientError(): boolean {
    return !!this.statusCode && this.statusCode >= 400 && this.statusCode < 500;
  }

  isServerError(): boolean {
    return !!this.statusCode && this.statusCode >= 500;
  }

  isRetryable(): boolean {
    // ניסיון חוזר על: שגיאות רשת, 408, 429, 5xx
    if (this.isNetworkError()) return true;
    if (this.statusCode === 408 || this.statusCode === 429) return true;
    if (this.isServerError()) return true;
    return false;
  }
}

דוגמת תבנית שירות

// assets/src/services/chatService.ts
class ChatService {
  private readonly baseEndpoint = '/woo-ai/v1/chat';

  async sendMessage(
    message: string,
    sessionId?: string,
    context?: ChatContext
  ): Promise<ChatResponse> {
    validateChatRequest({ message, sessionId, context });

    const response = await fetchAPI<WPChatMessageResponse>(
      `${this.baseEndpoint}/message`,
      {
        method: 'POST',
        body: JSON.stringify({
          message,
          session_id: sessionId,
          context,
        }),
      }
    );

    return {
      sessionId: response.session_id,
      message: response.message?.content || '',
      products: response.products || [],
      suggestions: response.suggestions || [],
    };
  }

  async getOrCreateSession(context?: ChatContext): Promise<string> {
    // בדיקת localStorage קודם
    const storedSessionId = getSessionId();
    if (storedSessionId) {
      const session = await this.getSessionById(storedSessionId);
      if (session) return storedSessionId;
    }

    // יצירת סשן חדש
    const response = await fetchAPI<{ session_id: string }>(
      `${this.baseEndpoint}/session`,
      { method: 'POST' }
    );

    saveSessionId(response.session_id);
    return response.session_id;
  }
}

export default new ChatService();

7. עיצוב

תצורת Tailwind

// tailwind.config.js
module.exports = {
  darkMode: ['class'],
  content: [
    './assets/src/**/*.{ts,tsx,js,jsx}',
    './includes/**/*.php',
    './templates/**/*.php',
  ],

  // קריטי: קידומת מחלקה לווידג'ט לבידוד עיצוב
  prefix: 'wac-',

  // safelist למחלקות דינמיות
  safelist: [
    'wac-rounded', 'wac-rounded-md', 'wac-rounded-lg',
    'wac-shadow', 'wac-shadow-md', 'wac-shadow-lg',
    'hover:wac-shadow-lg', 'hover:wac-bg-gray-100',
    // ... עוד מחלקות דינמיות
  ],

  theme: {
    extend: {
      colors: {
        // אינטגרציה של CSS custom properties
        border: 'hsl(var(--wac-border))',
        primary: {
          DEFAULT: 'hsl(var(--wac-primary))',
          foreground: 'hsl(var(--wac-primary-foreground))',
        },
        // צבעי אדמין של WordPress
        'wp-admin-blue': '#2271b1',
        'wp-admin-gray': '#f0f0f1',
        // צבעים ספציפיים לצ'אט
        'chat-user': '#0084ff',
        'chat-bot': '#f0f0f0',
      },
      zIndex: {
        'chat-widget': '9999',
        'wp-admin-bar': '99999',
      },
    },
  },

  plugins: [
    // תמיכת RTL
    require('tailwindcss/plugin')(({ addVariant }) => {
      addVariant('rtl', '[dir="rtl"] &');
      addVariant('ltr', '[dir="ltr"] &');
    }),
  ],
};

הוספת קידומת למחלקות הווידג’ט

פונקציית wac() מבטיחה שכל מחלקות Tailwind מקבלות קידומת:

// assets/src/chat/utils/wacClassNames.ts
const WAC_PREFIX = 'wac-';

const prefixToken = (token: string): string => {
  if (!token) return '';

  const isImportant = token.startsWith('!');
  const cleanToken = isImportant ? token.slice(1) : token;
  const segments = cleanToken.split(':');

  // קידומת רק למחלקת ה-utility, לא ל-variant
  const prefixedSegments = segments.map((segment, index) => {
    if (index === segments.length - 1) {
      return segment.startsWith(WAC_PREFIX) ? segment : `${WAC_PREFIX}${segment}`;
    }
    return segment;
  });

  const prefixed = prefixedSegments.join(':');
  return isImportant ? `!${prefixed}` : prefixed;
};

export const wac = (
  ...classLists: Array<string | false | null | undefined>
): string => {
  return classLists
    .filter(Boolean)
    .map((item) => normalizeInput(item as string))
    .join(' ');
};

// שימוש:
// wac('px-4 py-2 hover:bg-gray-100')
// => 'wac-px-4 wac-py-2 hover:wac-bg-gray-100'

תמיכת RTL (עברית)

// קומפוננטה עם מודעות ל-RTL
const { isRTL, direction } = useTranslation();

return (
  <div style={{ direction: direction as 'ltr' | 'rtl' }}>
    <input
      style={{
        paddingInlineStart: '44px',  // שימוש בתכונות לוגיות
        paddingInlineEnd: '112px',
      }}
    />
    <div style={isRTL ? { right: '8px' } : { left: '8px' }}>
      {/* אלמנט ממוקם */}
    </div>
  </div>
);

CSS Custom Properties לעיצוב נושא

/* CSS custom properties של הווידג'ט */
:root {
  --waa-primary: #6366f1;
  --waa-primary-light: #818cf8;
  --waa-primary-dark: #4f46e5;
  --waa-primary-50: #eef2ff;

  --waa-gray-50: #f9fafb;
  --waa-gray-100: #f3f4f6;
  --waa-gray-200: #e5e7eb;
  --waa-gray-300: #d1d5db;
  --waa-gray-400: #9ca3af;
  --waa-gray-500: #6b7280;
  --waa-gray-600: #4b5563;
  --waa-gray-700: #374151;
  --waa-gray-900: #111827;

  --waa-chat-user-bg: var(--waa-primary);
  --waa-chat-bot-bg: var(--waa-gray-100);
  --waa-chat-bot-text: var(--waa-gray-900);
}

8. TypeScript

תצורת מצב קפדני

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "module": "ESNext",
    "moduleResolution": "node",
    "jsx": "react-jsx",

    // בדיקת טיפוסים קפדנית
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true,
    "forceConsistentCasingInFileNames": true,

    // כינויי נתיבים
    "baseUrl": ".",
    "paths": {
      "@/*": ["assets/src/*"],
      "@admin/*": ["assets/src/admin/*"],
      "@chat/*": ["assets/src/chat/*"],
      "@components/*": ["assets/src/components/*"],
      "@hooks/*": ["assets/src/hooks/*"],
      "@utils/*": ["assets/src/utils/*"],
      "@types/*": ["assets/src/types/*"]
    }
  }
}

תבניות הגדרת טיפוסים

// assets/src/types/api.ts

// טיפוסי בקשה/תגובה של API
export interface ChatMessageRequest {
  session_id: string;
  message: string;
}

export interface ChatMessageResponse {
  success: boolean;
  session_id: string;
  message: {
    role: 'assistant';
    content: string;
  };
  context: SessionContext;
}

// עטיפת API גנרית
export type APIResponse<T> = SuccessResponse<T> | ErrorResponse;

// Type guards
export function isErrorResponse(
  response: SuccessResponse | ErrorResponse
): response is ErrorResponse {
  return response.success === false;
}

טיפוסים גלובליים של WordPress

// assets/src/types/global.d.ts
declare global {
  interface Window {
    wooAiChatbot?: {
      nonce: string;
      apiUrl: string;
      pluginUrl: string;
      currentPage: string;
      welcomeMessage?: string;
    };
    wooAIChatbot?: Window['wooAiChatbot'];
    wooAIChatConfig?: {
      appearance?: AppearanceConfig;
      initialMessage?: string;
    };
  }
}

9. תהליך פיתוח

פיתוח במצב Watch

# הפעלת שרת פיתוח עם hot reload
npm run dev

# זה מריץ:
# webpack --mode development --watch

מצב watch מספק:
– בניות מצטברות מהירות (~500ms בשמירה)
– Source maps לדיבאגינג
– ללא מיזעור לפלט קריא
– מצב transpile-only של TypeScript (דילוג על בדיקת טיפוסים למהירות)

בדיקת טיפוסים בנפרד

# הרצת בדיקת טיפוסים באופן עצמאי (CI/pre-commit)
npm run typecheck

# זה מריץ:
# tsc --noEmit

בניית פרודקשן

# בנייה לפרודקשן
npm run build

# זה כולל:
# - בדיקת טיפוסים מלאה
# - מיזעור (Terser)
# - הסרת console logs
# - אופטימיזציית CSS
# - Source maps

טיפים לדיבאגינג

  1. React DevTools: התקינו את התוסף לדפדפן לבדיקת קומפוננטות
  2. Redux DevTools: לא בשימוש (מבוסס Context), אבל אפשר להוסיף interceptors ללוגינג
  3. Network Tab: סננו לפי /wp-json/woo-ai/ לראות קריאות API
  4. Console Logging: השתמשו ב-logger.debug() ללוגינג מותנה
// assets/src/utils/logger.ts
export const logger = {
  debug: (...args: unknown[]) => {
    if (process.env.NODE_ENV === 'development') {
      console.log('[WAC]', ...args);
    }
  },
  error: (...args: unknown[]) => console.error('[WAC]', ...args),
};
  1. WordPress Debug: הפעילו WP_DEBUG לראות שגיאות PHP לצד JS

10. הוספת פיצ’רים חדשים

הוספת עמוד אדמין חדש

  1. יצירת קומפוננטת העמוד:
// assets/src/admin/pages/NewFeature.tsx
import React from 'react';
import { Card, CardHeader, CardTitle, CardContent } from '@components/ui/card';
import { useSettings } from '@/contexts/SettingsContext';

export const NewFeature: React.FC = () => {
  const { settings, updateSettings, loading } = useSettings();

  if (loading) {
    return <div>Loading...</div>;
  }

  return (
    <div className="p-6 space-y-6">
      <h1 className="text-2xl font-semibold">New Feature</h1>
      <Card>
        <CardHeader>
          <CardTitle>Feature Settings</CardTitle>
        </CardHeader>
        <CardContent>
          {/* תוכן הפיצ'ר */}
        </CardContent>
      </Card>
    </div>
  );
};
  1. הוספה לטיפוסי האדמין:
// assets/src/admin/types.ts
export type AdminPage =
  | 'dashboard'
  | 'ai-providers'
  | 'new-feature'  // הוסיפו כאן
  // ...
  1. רישום ב-index.tsx:
// assets/src/admin/index.tsx
import { NewFeature } from './pages/NewFeature';

const pageSlugMap: Partial<Record<AdminPage, string>> = {
  // ...
  'new-feature': 'woo-ai-chatbot-new-feature',
};

const renderPage = () => {
  switch (currentPage) {
    // ...
    case 'new-feature':
      return <NewFeature />;
  }
};
  1. רישום פריט תפריט ב-PHP (ב-PHP):
// includes/admin/class-admin-menu.php
add_submenu_page(
    'woo-ai-chatbot',
    'New Feature',
    'New Feature',
    'manage_options',
    'woo-ai-chatbot-new-feature',
    [$this, 'render_react_admin']
);

הוספת קומפוננטת ווידג’ט חדשה

  1. יצירת הקומפוננטה:
// assets/src/chat/components/NewWidget.tsx
import React from 'react';
import { wac } from '../utils/wacClassNames';
import { useChat } from '@/contexts/ChatContext';

interface NewWidgetProps {
  onAction: () => void;
}

export const NewWidget: React.FC<NewWidgetProps> = ({ onAction }) => {
  const { sessionId } = useChat();

  return (
    <div className={wac('p-4 bg-white rounded-lg shadow-md')}>
      <button
        onClick={onAction}
        className={wac('px-4 py-2 bg-primary text-white rounded hover:bg-primary/90')}
      >
        Action
      </button>
    </div>
  );
};
  1. ייצוא מ-index:
// assets/src/chat/components/index.ts
export { NewWidget } from './NewWidget';
  1. שימוש ב-ChatWidgetBottom:
import { NewWidget } from './NewWidget';

// בעץ הקומפוננטות
{showNewWidget && (
  <NewWidget onAction={handleWidgetAction} />
)}

הוספת Context חדש

  1. יצירת ה-context:
// assets/src/contexts/NewContext.tsx
import { createContext, useContext, useState, useCallback } from 'react';

interface NewContextValue {
  data: DataType | null;
  loading: boolean;
  error: Error | null;
  fetchData: () => Promise<void>;
  updateData: (updates: Partial<DataType>) => Promise<void>;
}

const NewContext = createContext<NewContextValue | undefined>(undefined);

export const NewProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
  const [data, setData] = useState<DataType | null>(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState<Error | null>(null);

  const fetchData = useCallback(async () => {
    setLoading(true);
    try {
      const result = await apiClient.get<DataType>('/endpoint');
      setData(result);
    } catch (err) {
      setError(err as Error);
    } finally {
      setLoading(false);
    }
  }, []);

  const updateData = useCallback(async (updates: Partial<DataType>) => {
    // תבנית עדכון אופטימיסטי
    const previous = data;
    setData((prev) => ({ ...prev, ...updates }));

    try {
      const result = await apiClient.post<DataType>('/endpoint', updates);
      setData(result);
    } catch (err) {
      setData(previous); // Rollback
      throw err;
    }
  }, [data]);

  return (
    <NewContext.Provider value={{ data, loading, error, fetchData, updateData }}>
      {children}
    </NewContext.Provider>
  );
};

export const useNewContext = (): NewContextValue => {
  const context = useContext(NewContext);
  if (context === undefined) {
    throw new Error('useNewContext must be used within a NewProvider');
  }
  return context;
};
  1. הוספה לעץ ה-providers בנקודת הכניסה המתאימה.

נספח: התייחסות לתבניות נפוצות

טופס עם React Hook Form

import { useForm } from 'react-hook-form';
import { Form, FormField, FormItem, FormLabel, FormControl } from '@components/ui/form';

const MyForm = () => {
  const form = useForm<FormData>({
    defaultValues: { name: '', email: '' },
  });

  const onSubmit = async (data: FormData) => {
    await saveData(data);
  };

  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
            </FormItem>
          )}
        />
        <Button type="submit">Save</Button>
      </form>
    </Form>
  );
};

תבנית טעינת נתונים אסינכרונית

const MyComponent = () => {
  const [data, setData] = useState<Data | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const load = async () => {
      try {
        const result = await fetchData();
        setData(result);
      } catch (err) {
        setError(err as Error);
      } finally {
        setLoading(false);
      }
    };
    load();
  }, []);

  if (loading) return <Skeleton />;
  if (error) return <Alert variant="destructive">{error.message}</Alert>;
  if (!data) return null;

  return <div>{/* רינדור הנתונים */}</div>;
};

עדכון אחרון: דצמבר 2024
גרסה: 0.2.1