מדריך פיתוח 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. שווה את השקט הנפשי.
דברים שיתפסו אתכם
- Hot reload לא עובד עם WordPress – פשוט קבלו את זה, הריצו
npm run devורעננו ידנית - אל תשתמשו ב-
window.fetch– השתמשו בעטיפתapiClientשלנו, היא מטפלת ב-nonces וניסיונות חוזרים - מחלקות Tailwind נמחקות – אם המחלקה הדינמית שלכם לא עובדת, בדקו את safelist ב-
tailwind.config.js - 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
טיפים לדיבאגינג
- React DevTools: התקינו את התוסף לדפדפן לבדיקת קומפוננטות
- Redux DevTools: לא בשימוש (מבוסס Context), אבל אפשר להוסיף interceptors ללוגינג
- Network Tab: סננו לפי
/wp-json/woo-ai/לראות קריאות API - 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),
};
- WordPress Debug: הפעילו
WP_DEBUGלראות שגיאות PHP לצד JS
10. הוספת פיצ’רים חדשים
הוספת עמוד אדמין חדש
- יצירת קומפוננטת העמוד:
// 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>
);
};
- הוספה לטיפוסי האדמין:
// assets/src/admin/types.ts
export type AdminPage =
| 'dashboard'
| 'ai-providers'
| 'new-feature' // הוסיפו כאן
// ...
- רישום ב-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 />;
}
};
- רישום פריט תפריט ב-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']
);
הוספת קומפוננטת ווידג’ט חדשה
- יצירת הקומפוננטה:
// 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>
);
};
- ייצוא מ-index:
// assets/src/chat/components/index.ts
export { NewWidget } from './NewWidget';
- שימוש ב-ChatWidgetBottom:
import { NewWidget } from './NewWidget';
// בעץ הקומפוננטות
{showNewWidget && (
<NewWidget onAction={handleWidgetAction} />
)}
הוספת Context חדש
- יצירת ה-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;
};
- הוספה לעץ ה-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

