@codesinger0/shared-components 1.0.21 → 1.0.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ import React, { useState } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+
4
+ const QAAccordion = ({
5
+ title = "שאלות נפוצות",
6
+ subtitle = "מענה לשאלות הנפוצות ביותר",
7
+ qaItems = [],
8
+ className = ""
9
+ }) => {
10
+ const [openIndex, setOpenIndex] = useState(null);
11
+
12
+ const toggleAccordion = (index) => {
13
+ setOpenIndex(openIndex === index ? null : index);
14
+ };
15
+
16
+ // Default Q&A items for demo
17
+ const defaultQAItems = [
18
+ {
19
+ question: "מהי טרופיקל ג’אר ומה הסיפור שמאחוריה?",
20
+ answer: "טרופיקל ג’אר היא מותג של רטבים מותססים טבעיים בעבודת יד, שנולדה מתוך אהבה לתסיסה, לטעמים טרופיים ולבריאות. אנו משלבים מסורת קולינרית עם יצירתיות מודרנית, כדי להביא לשולחן שלכם משהו חדש ומרגש."
21
+ },
22
+ {
23
+ question: "מה זה רוטב מותסס?",
24
+ answer: "רוטב מותסס הוא רוטב שעובר תהליך טבעי שבו מיקרואורגניזמים ידידותיים מפרקים את הסוכרים בירקות ובפירות. התוצאה היא עומק טעמים ייחודי, עיכול קל יותר ויתרונות בריאותיים — בלי חומרים משמרים."
25
+ },
26
+ {
27
+ question: "מה המרכיבים ברטבים שלכם?",
28
+ answer: "כל רוטב מיוצר מירקות ופירות טריים, תבלינים איכותיים, מלח ים ומים. בלי חומרים מלאכותיים, בלי צבעי מאכל ובלי קיצורי דרך — רק חומרי גלם טבעיים שמביאים לידי ביטוי את הטבע."
29
+ },
30
+ {
31
+ question: "איך הכי מומלץ להשתמש ברטבים?",
32
+ answer: "הרטבים שלנו מתאימים כמעט לכל דבר: כתוספת לסלטים, כמטבל לירקות, לשדרוג סנדוויצ'ים, כתיבול לבשרים ולדגים, או אפילו כתיבול עדין לאורז ופסטה. הם מוסיפים נגיעה טרופית ובריאה לכל מנה."
33
+ },
34
+ {
35
+ question: "איך לשמור את הרטבים אחרי פתיחה?",
36
+ answer: "לאחר פתיחה יש לשמור במקרר ולוודא שהצנצנת סגורה היטב. התסיסה ממשיכה גם אחרי הפתיחה, לכן ייתכנו שינויים קלים בטעם או בבועות טבעיות — זה חלק מהקסם!"
37
+ },
38
+ {
39
+ question: "האם הרטבים מתאימים לטבעונים?",
40
+ answer: "כן, כל הרטבים שלנו 100% טבעוניים וצמחיים. הם מיוצרים ללא מוצרים מן החי ומתאימים לכל מי שמחפש תיבול אותנטי, טבעי ובריא."
41
+ },
42
+ {
43
+ question: "האם הרטבים חריפים?",
44
+ answer: "הסדרה שלנו כוללת גם רטבים עדינים וגם רטבים חריפים יותר. בכל מוצר מצוין רמת החריפות כדי שתוכלו לבחור את מה שהכי מתאים לטעם האישי שלכם."
45
+ },
46
+ {
47
+ question: "האם אפשר להזמין משלוחים?",
48
+ answer: "כן! אנו מבצעים משלוחים לכל הארץ. ניתן לבחור משלוח עד הבית או נקודת איסוף, והזמנה מגיעה לרוב תוך 3–5 ימי עסקים."
49
+ },
50
+ {
51
+ question: "האם אפשר לרכוש בחנויות פיזיות?",
52
+ answer: "נכון לעכשיו, הרטבים זמינים בעיקר כאן באתר. בהמשך נוסיף נקודות מכירה נבחרות ונעדכן באתר וברשתות החברתיות."
53
+ },
54
+ {
55
+ question: "כמה זמן מחזיקים הרטבים?",
56
+ answer: "בזכות התסיסה הטבעית, חיי המדף ארוכים יותר מרטבים רגילים. כל צנצנת מגיעה עם תאריך עדיף להשתמש עד, ולאחר פתיחה מומלץ לצרוך תוך מספר שבועות."
57
+ },
58
+ {
59
+ question: "מה היתרונות הבריאותיים של רטבים מותססים?",
60
+ answer: "התסיסה מייצרת חיידקים פרוביוטיים טובים ותורמת לעיכול בריא. בנוסף, היא מעצימה את הטעמים באופן טבעי ומפחיתה צורך בתוספים מלאכותיים."
61
+ },
62
+ {
63
+ question: "איך אני יודע איזה רוטב לבחור?",
64
+ answer: "אם אתם אוהבים טעמים עדינים – התחילו ברטבים הפירותיים והמתונים. אם אתם חובבי חריף – נסו את הגרסאות החריפות יותר. תמיד כדאי להתחיל מצנצנת אחת ולגלות את הסגנון האהוב עליכם."
65
+ }
66
+ ];
67
+
68
+
69
+ const items = qaItems.length > 0 ? qaItems : defaultQAItems;
70
+
71
+ return (
72
+ <section className={`py-16 px-4 bg-main ${className}`} dir="rtl">
73
+ <div className="max-w-4xl mx-auto">
74
+ {/* Header */}
75
+ <div className="text-center mb-12">
76
+ <h2 className="title mb-4">{title}</h2>
77
+ <p className="subtitle">{subtitle}</p>
78
+ </div>
79
+
80
+ {/* Accordion */}
81
+ <div className="space-y-4">
82
+ {items.map((item, index) => (
83
+ <div
84
+ key={index}
85
+ className="bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden"
86
+ >
87
+ {/* Question Header */}
88
+ <button
89
+ onClick={() => toggleAccordion(index)}
90
+ className="w-full px-6 py-5 text-right flex items-center justify-between hover:bg-gray-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-primary focus:ring-opacity-20"
91
+ >
92
+ <span className="subtitle font-semibold text-gray-800 flex-1">
93
+ {item.question}
94
+ </span>
95
+
96
+ {/* Animated Arrow */}
97
+ <motion.div
98
+ animate={{ rotate: openIndex === index ? 180 : 0 }}
99
+ transition={{
100
+ type: "spring",
101
+ stiffness: 300,
102
+ damping: 25
103
+ }}
104
+ className="mr-4 flex-shrink-0"
105
+ >
106
+ <svg
107
+ className="w-5 h-5 text-primary"
108
+ fill="none"
109
+ stroke="currentColor"
110
+ viewBox="0 0 24 24"
111
+ >
112
+ <path
113
+ strokeLinecap="round"
114
+ strokeLinejoin="round"
115
+ strokeWidth={2}
116
+ d="M19 9l-7 7-7-7"
117
+ />
118
+ </svg>
119
+ </motion.div>
120
+ </button>
121
+
122
+ {/* Answer Content with Animation */}
123
+ <AnimatePresence initial={false}>
124
+ {openIndex === index && (
125
+ <motion.div
126
+ initial={{ height: 0, opacity: 0 }}
127
+ animate={{
128
+ height: "auto",
129
+ opacity: 1,
130
+ transition: {
131
+ height: {
132
+ type: "spring",
133
+ stiffness: 300,
134
+ damping: 30,
135
+ mass: 0.8
136
+ },
137
+ opacity: {
138
+ duration: 0.25,
139
+ delay: 0.1
140
+ }
141
+ }
142
+ }}
143
+ exit={{
144
+ height: 0,
145
+ opacity: 0,
146
+ transition: {
147
+ height: {
148
+ type: "spring",
149
+ stiffness: 300,
150
+ damping: 30,
151
+ mass: 0.8
152
+ },
153
+ opacity: {
154
+ duration: 0.15
155
+ }
156
+ }
157
+ }}
158
+ className="overflow-hidden"
159
+ >
160
+ <div className="px-6 pb-5 border-t border-gray-100">
161
+ <motion.p
162
+ initial={{ y: -10, opacity: 0 }}
163
+ animate={{
164
+ y: 0,
165
+ opacity: 1,
166
+ transition: {
167
+ type: "spring",
168
+ stiffness: 300,
169
+ damping: 25,
170
+ delay: 0.1
171
+ }
172
+ }}
173
+ className="content-text text-gray-700 leading-relaxed pt-4"
174
+ >
175
+ {item.answer}
176
+ </motion.p>
177
+ </div>
178
+ </motion.div>
179
+ )}
180
+ </AnimatePresence>
181
+ </div>
182
+ ))}
183
+ </div>
184
+
185
+ {/* Optional CTA */}
186
+ <div className="text-center mt-12">
187
+ <p className="content-text mb-4">
188
+ לא מצאתם את התשובה שחיפשתם?
189
+ </p>
190
+ <button className="btn-primary">
191
+ צרו קשר
192
+ </button>
193
+ </div>
194
+ </div>
195
+ </section>
196
+ );
197
+ };
198
+
199
+ export default QAAccordion;
@@ -0,0 +1,38 @@
1
+ // components/ToastProvider.jsx
2
+ import { createContext, useContext, useState, useCallback } from 'react';
3
+ import Toast from './elements/Toast';
4
+
5
+ const ToastContext = createContext();
6
+
7
+ export const useToast = () => useContext(ToastContext);
8
+
9
+ export const ToastProvider = ({ children }) => {
10
+ const [toasts, setToasts] = useState([]);
11
+
12
+ const addToast = useCallback((message, variant, duration) => {
13
+ const id = Date.now();
14
+ setToasts((prev) => [...prev, { id, message, duration, variant }]);
15
+ }, []);
16
+
17
+ const removeToast = useCallback((id) => {
18
+ setToasts((prev) => prev.filter((t) => t.id !== id));
19
+ }, []);
20
+
21
+ return (
22
+ <ToastContext.Provider value={addToast}>
23
+ {children}
24
+ <div className="fixed bottom-5 left-0 right-0 flex flex-col items-center space-y-2 z-50">
25
+ {toasts.map((toast) => (
26
+ <Toast
27
+ key={toast.id}
28
+ id={toast.id}
29
+ message={toast.message}
30
+ duration={toast.duration}
31
+ variant={toast.variant}
32
+ onClose={removeToast}
33
+ />
34
+ ))}
35
+ </div>
36
+ </ToastContext.Provider>
37
+ );
38
+ };
@@ -0,0 +1,37 @@
1
+ import { useEffect, useState } from 'react';
2
+
3
+ const variantStyles = {
4
+ success: 'bg-green-500 text-white',
5
+ error: 'bg-red-500 text-white',
6
+ info: 'bg-blue-500 text-white',
7
+ warning: 'bg-yellow-400 text-black',
8
+ };
9
+
10
+ const Toast = ({ id, message, onClose, duration = 3000, variant = 'info' }) => {
11
+ const [show, setShow] = useState(true);
12
+
13
+ useEffect(() => {
14
+ const timer = setTimeout(() => {
15
+ setShow(false);
16
+ setTimeout(() => onClose(id), 300); // match transition duration
17
+ }, duration);
18
+
19
+ return () => clearTimeout(timer);
20
+ }, [id, onClose, duration]);
21
+
22
+ return (
23
+ <div
24
+ className={`fixed bottom-5 left-1/2 transform -translate-x-1/2
25
+ px-4 py-2 rounded-lg shadow-lg
26
+ text-sm sm:text-base md:text-lg
27
+ max-w-xs sm:max-w-sm md:max-w-md
28
+ transition-all duration-300
29
+ ${variantStyles[variant]}
30
+ ${show ? 'opacity-100 translate-y-0' : 'opacity-0 translate-y-5'}`}
31
+ >
32
+ {message}
33
+ </div>
34
+ );
35
+ };
36
+
37
+ export default Toast;
package/dist/index.js CHANGED
@@ -5,12 +5,14 @@ export { default as LargeItemCard } from './components/LargeItemCard';
5
5
  export { default as SmallItemsGrid } from './components/SmallItemsGrid';
6
6
  export { default as SmallItemCard } from './components/SmallItemCard';
7
7
  export { default as Hero } from './components/Hero'
8
+ export { default as QAAccordion } from './components/QAAccordion'
8
9
 
9
10
  // Modals
10
11
  export { default as ItemDetailsModal } from './components/modals/ItemDetailsModal'
11
12
 
12
13
  // Context
13
14
  export { ItemModalProvider, useItemModal } from './context/ItemModalContext';
15
+ export { ToastProvider, useToast } from './components/ToastProvider'
14
16
 
15
17
  // Hooks
16
18
  export { default as useScrollLock } from './hooks/useScrollLock'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codesinger0/shared-components",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
4
4
  "description": "Shared React components for customer projects",
5
5
  "main": "dist/index.js",
6
6
  "files": [