🏗️

ספרינט 1

בסיס + תפילות
Anu Banecha Donations — תוכנית ויזואלית לג'וניורות
📅 שבועות 1–2 👩‍💻 2 ג'וניורות 🎯 6 משימות 🏷️ ABD-5 → ABD-10 מטרה: רשימת תפילות חיה
🎯 מטרת הספרינט PostgreSQL רץ מקומית, 5 תפילות בעברית ב-DB, ה-API מחזיר נתונים אמיתיים, והאפליקציה מציגה את רשימת התפילות עם ניווט לעמוד תפילה שמציג כפתורי תרומה סטטיים.

📋 תוכן עניינים

  1. מה קיים ↔ מה ייבנה
  2. גרף תלויות — סדר עבודה
  3. ציר זמן — 2 שבועות
  4. מה נוצר בסוף הספרינט
  5. Prereq — הכנת סביבת פיתוח
  6. F2 🚨 BLOCKER — Migration + Seed
  7. F3 — Prayers API Backend
  8. F4 — App Foundation (_layout)
  9. F5 — Home Screen: רשימת תפילות
  10. F6 — Prayer Page + DonationWidget
  11. נקודות סנכרון
  12. ה-App בסוף הספרינט
  13. הגדרת "סיום"
1

מה קיים ↔ מה ייבנה

ה-Scaffold כבר מכיל הרבה קוד — אל תכתבו מחדש דברים שקיימים. המשימות של הספרינט ממלאות פערים ספציפיים מאוד.

Backend — מה קיים vs מה ספרינט 1 מוסיף

קובץ / רכיבמצב עכשיומה ספרינט 1 עושה
main.py✅ מלאלא נוגעים
models/models.py⚠️ חסר עמודהF2: מוסיפות prayer_name ל-Donation
schemas/schemas.py⚠️ PrayerResponse חלקיF3: מרחיבות PrayerResponse ל-8 שדות
alembic/versions/❌ ריקF2: יוצרות migration ראשוני — כל 6 טבלאות
scripts/seed.py❌ לא קייםF2: כותבות seed עם 5 קטגוריות + 5 תפילות
services/prayer_service.py❌ NotImplementedErrorF3: מממשות 4 פונקציות + helper _localize
routers/prayers.py❌ מחזיר רשימות ריקותF3: מחברות ל-service, 4 endpoints אמיתיים
middleware/auth.py🔜 ספרינט 3לא נוגעים
services/stripe_service.py🔜 ספרינט 2לא נוגעים

Frontend — מה קיים vs מה ספרינט 1 מוסיף

קובץ / רכיבמצב עכשיומה ספרינט 1 עושה
services/api.ts✅ מלאלא נוגעים — קומפוננטת דוגמא
store/authStore.ts✅ מלאלא נוגעים — קומפוננטת דוגמא
store/donationStore.ts✅ מלאלא נוגעים — F6 קוראת לו
constants/*✅ מלאלא נוגעים
types/*✅ מלאלא נוגעים
components/PrayerCard/✅ מלאלא נוגעים — קומפוננטת דוגמא
app/_layout.tsx⚠️ חסרות 3 שורותF4: מוסיפות signInAnon + StripeProvider + useAuth
hooks/usePrayer.ts❌ stub ריקF5: ממלאות usePrayers() | F6: ממלאות usePrayer(slug)
app/(tabs)/index.tsx⚠️ TODO ברשימהF5: FlatList עם PrayerCard + loading/error
app/prayer/[slug].tsx⚠️ חסר not-found + polishF6: מצב not-found, layout מסודר
components/DonationWidget/❌ TODO stubF6: כפתורי tier עם highlight, כפתור "תרום" ריק
components/DonationBottomSheet/🔜 ספרינט 2לא נוגעים
components/QuickButtons/🔜 ספרינט 3stub בלבד — לא קורסת
2

גרף תלויות — סדר עבודה

🚨 כלל הברזל: F2 (Migration + Seed) חייבת להתמזג ל-main לפני שמתחילים F3. F3 חייבת להתמזג לפני שמסיימות F5. F5 לפני F6. לא מדלגות על הסדר הזה.
🟣 Prereq — Dev Setup ABD-5 · שתי המפתחות
🚨 F2 — Migration + Seed ABD-6 · M · BLOCKER
🟣 F4 — App Foundation ABD-8 · S · עצמאית
🔵 F3 — Prayers API ABD-7 · M · Backend
🟢 F5 — Home Screen: Prayer List ABD-9 · M · תלוי F3 + F4
🟢 F6 — Prayer Page + DonationWidget ABD-10 · M · תלוי F5

טבלת בחירת משימות — Self-Select

משימהJiraגודלתלויה במי יכולה לבחור
Prereq — Dev SetupABD-5S שתי המפתחות — חובה
🚨 F2 — Migration + SeedABD-6M Prereqקחי ראשונה — gates הכל
F3 — Prayers APIABD-7M F2זמינה לאחר מיזוג F2
F4 — App FoundationABD-8S Prereqעצמאית — תוכלי להתחיל מיד
F5 — Home ScreenABD-9M F3 + F4UI ניתן לפתח עם mock data לפני F3
F6 — Prayer PageABD-10M F5זמינה לאחר מיזוג F5
חלוקת עבודה מומלצת: מפתחת 1 לוקחת F2 → F3 (backend-first). מפתחת 2 לוקחת F4 → F5 עם mock data → F6. ב-Day 6 בערך F3 נמזג ו-F5 עובר ל-API אמיתי.
3

ציר זמן — 2 שבועות

0

יום 0 — לפני שמתחילות

שתי המפתחות מקיימות את Prereq: Docker עולה, Backend רץ, Frontend עולה, שתיהן מאשרות GET /health → 200 ו-Expo web טוען. גם: שיחת 5 דקות על contract של PrayerResponse ↔ LocalizedPrayer.

1–3

ימים 1–3 — F2 🚨 + F4

מפתחת 1: מוסיפה prayer_name ל-Donation model, מריצה Alembic autogenerate, בודקת את קובץ ה-migration, מריצה upgrade head, כותבת seed.py ומריצה אותו. 5 תפילות ב-DB. פותחת PR → review → מיזוג.

מפתחת 2: עובדת על F4 — מוסיפה signInAnon() + StripeProvider + useAuth() ל-_layout.tsx (פחות מיום). אחרי זה מתחילה F5 עם mock data.

4–6

ימים 4–6 — F3 + F5

מפתחת 1: F3 — מרחיבה PrayerResponse, כותבת _localize() helper, מממשת 4 service functions, מחברת router. מאשרת: GET /api/prayers → 5 תפילות.

מפתחת 2: F5 — מוסיפה usePrayers() hook, מחברת FlatList ל-index.tsx עם loading/error states. ברגע שF3 נמזג — מסירה mock ועוברת ל-API אמיתי.

7–10

ימים 7–10 — F6 + בדיקות

אחרי מיזוג F5: אחת מהמפתחות (או שתיהן יחד) עובדת על F6 — ממלאת usePrayer(slug), מסיימת את עמוד התפילה, מממשת DonationWidget עם כפתורי tier + highlight.

בסוף שבוע 2: flow שלם מהבית → תפילה → כפתורי תרומה עובד. כל ה-PRs עברו review + אישור של שרה.

4

מה נוצר בסוף הספרינט

קבצים חדשים שנכתבים מאפס

Backend · F2
alembic/versions/xxxx_initial.py
migration ראשוני — יוצר 6 טבלאות כולל prayer_name
Backend · F2
scripts/seed.py
seed: 5 קטגוריות + 5 תפילות בעברית ב-DB

קבצים קיימים שמשתנים

קובץשינוימי
backend/app/models/models.pyשורה אחת: prayer_name = Column(String, nullable=True)F2
backend/app/schemas/schemas.pyPrayerResponse מורחב + prayer_name ב-DonationCreateF2+F3
backend/app/services/prayer_service.py4 functions + _localize helper (מ-NotImplementedError לקוד אמיתי)F3
backend/app/routers/prayers.py4 endpoints מחוברים לservice (מ-stub לעובד)F3
prayers-app/app/_layout.tsxsignInAnon() + StripeProvider + useAuth() (3 הוספות קטנות)F4
prayers-app/hooks/usePrayer.tsusePrayers() נוסף + usePrayer(slug) ממומשF5+F6
prayers-app/app/(tabs)/index.tsxFlatList + LoadingSpinner + error stateF5
prayers-app/app/prayer/[slug].tsxnot-found state + layout polishF6
prayers-app/components/DonationWidget/DonationWidget.tsxtier buttons עם selection highlight + כפתור תרום (ריק)F6
prayers-app/i18n/he.jsonמפתחות: error.loading, prayer.not_found, common.back, donation.donateF5+F6

מה שעובד בסוף הספרינט — end-to-end flow

🌐 פותחת את האפליקציה (web)
Firebase Anonymous Login 🔕 (ברקע, ללא UI)
📋 מסך הבית — רשימת 5 תפילות בעברית
לוחצת על "תפילה לרפואה שלמה"
📖 עמוד תפילה — כותרת + טקסט מלא
💰 DonationWidget — כפתורים: ₪18 / ₪36 / ₪72 / ₪180 / ₪360
לוחצת ₪72 → הכפתור מתהדר (highlighted)
לוחצת "תרום" → כלום (TODO Sprint 2) ✅ נורמלי
⚠️ מה לא עובד בסוף הספרינט (בכוונה): תשלום אמיתי (Sprint 2), כפתורי Quick Actions (Sprint 3), LanguagePicker/RTL (Sprint 4), חיפוש (Sprint 5). זה OK — ספרינט 1 הוא בסיס.
ABD-5
🟣 Prereq — הכנת סביבת פיתוח
S · שתי המפתחות · לפני הכל
זו לא משימה לבחירה — שתי המפתחות חייבות לסיים את זה לפני שמתחילים לפתח.

❌ לפני

  • Docker לא רץ / לא מותקן
  • לא נוצר .env מ-.env.example
  • אין Firebase service account JSON
  • האפליקציה לא עולה

✅ אחרי

  • GET localhost:8000/health → 200
  • localhost:8000/docs פתוח ועובד
  • Expo web רץ (npx expo start → w)
  • שתי המפתחות הראו זו לזו את /health

Backend — שלבים

cd backend
python -m venv .venv
.venv\Scripts\activate          # Windows
# source .venv/bin/activate     # Mac/Linux
pip install -r requirements.txt -r requirements-dev.txt

cp .env.example .env            # ואז ממלאות:
# DATABASE_URL=postgresql://user:password@localhost:5432/prayers_db
# STRIPE_SECRET_KEY=sk_test_...
# FIREBASE_CREDENTIALS_PATH=./firebase-service-account.json

docker-compose up -d            # PostgreSQL על port 5432
uvicorn app.main:app --reload   # Server על port 8000

# בדיקה:
curl http://localhost:8000/health   # → {"status": "ok"}

Frontend — שלבים

cd prayers-app
npm install
cp .env.example .env            # ואז ממלאות:
# EXPO_PUBLIC_API_URL=http://localhost:8000
# EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
# EXPO_PUBLIC_FIREBASE_API_KEY=...  (4 משתנים נוספים)

npx expo start
# → לוחצות w לפתוח web

⚠️ מלכודות נפוצות

  • Firebase: מורידות serviceAccountKey.json מ-Firebase Console → שמות ב-backend/firebase-service-account.jsonמוסיפות ל-.gitignore לפני כל commit
  • Stripe: sk_test_... הולך ל-backend/.env. pk_test_... הולך ל-prayers-app/.env. לא לבלבל.
  • EXPO_PUBLIC_* הולכים רק ל-prayers-app/.env, לא ל-backend/.env
  • Docker Desktop חייב לרוץ לפני docker-compose up
ABD-6
🚨 F2 — DB Migration + Seed (BLOCKER)
M · ~2–3 ימים · Gates F3, F5, F6
🚨 BLOCKER — בלי מיזוג של F2, אין טבלאות ב-DB → F3 לא יכולה להתחיל → F5 לא יכולה לסיים → F6 חסומה. קחי את זה ראשונה.

❌ לפני F2

  • alembic/versions/ ריק
  • אין טבלאות ב-PostgreSQL
  • prayer_name חסר מ-Donation
  • seed.py לא קיים

✅ אחרי F2

  • 6 טבלאות ב-DB (כולל prayer_name)
  • 5 קטגוריות + 5 תפילות בעברית ב-DB
  • GET /api/prayers → [] (ריק, עד F3)
  • seed.py אידמפוטנטי — בטוח לרוץ שוב

שלב 1 — הוספת prayer_name למודל

פותחות backend/app/models/models.py, מוצאות את class Donation (שורה ~65). אחרי donor_name:

# backend/app/models/models.py — בתוך class Donation:
prayer_name = Column(String, nullable=True)  # שם לתפילה — שונה מ-donor_name (שם לקבלה)

שלב 2 — הוספה ל-Schema

# backend/app/schemas/schemas.py — בתוך DonationCreate:
prayer_name: str | None = None

שלב 3 — Autogenerate migration

cd backend
alembic revision --autogenerate -m "initial"
# → נוצר backend/alembic/versions/xxxx_initial.py

# ⚠️ פתחי את הקובץ שנוצר ובדקי:
# ✅ op.create_table('prayers', ...)
# ✅ op.create_table('donations', ...) — כולל prayer_name
# ✅ 6 טבלאות: prayers, users, donations, categories, quick_buttons, recurring_donations
# אם prayer_name חסר → מחקי את הקובץ, חזרי לשלב 1

שלב 4 — Apply migration

alembic upgrade head
# → No errors

# ואז בדיקה ב-psql (או pgAdmin):
\dt   # → 6 טבלאות
SELECT column_name FROM information_schema.columns WHERE table_name='donations';
# → חייב להופיע: prayer_name

שלב 5 — כתיבת seed.py

יוצרות backend/scripts/seed.py (לא קיים). מריצות מתוך תיקיית backend:

cd backend
python scripts/seed.py
# → Categories ready: 5
# → Prayers seeded: 5 new

# בדיקה:
SELECT COUNT(*) FROM prayers;     # → 5
SELECT COUNT(*) FROM categories;  # → 5

✅ קריטריוני קבלה

  • prayer_name = Column(String, nullable=True) קיים ב-Donation model
  • קובץ migration קיים ב-alembic/versions/ ואוּמת ידנית
  • alembic upgrade head רץ ללא שגיאות
  • SELECT COUNT(*) FROM prayers → 5
  • SELECT COUNT(*) FROM categories → 5
  • הרצה כפולה של seed.py לא יוצרת כפילויות (אידמפוטנטי)
  • PR עבר review של המפתחת השנייה + אישור שרה
⚠️ מלכודת נפוצה: כשמריצות autogenerate, SQLAlchemy חייב להיות מחובר ל-DB (docker-compose up). אם alembic לא מוצא את המודלים — בדקי שה-venv פעיל ו-sys.path בـenv.py מכיל את app.
ABD-7
🔵 F3 — Prayers API Backend
M · ~2–3 ימים · תלוי F2

❌ לפני F3

  • service functions: raise NotImplementedError
  • Router מחזיר [] ו-{} ריקים
  • PrayerResponse: רק id+slug

✅ אחרי F3

  • GET /api/prayers?lang=he → 5 תפילות
  • GET /api/prayers/health → תפילה בודדת + view_count++
  • GET /api/prayers/search?q=רפואה → תוצאה
  • GET /api/prayers/category/health → תפילה

Contract קריטי — Backend ↔ Frontend

📋 לפני שמתחילות לבנות — שיחה של 5 דקות עם המפתחת שעובדת על F5. מאמתות שהשדות תואמים:
Backend PrayerResponseFrontend LocalizedPrayerמיפוי
ididstr(prayer.id)
slugslugישיר
titletitleמ-title_{lang}
bodybodyמ-body_{lang}
seo_descriptionseoDescriptioncamelCase בצד frontend
seo_keywordsseoKeywordscamelCase בצד frontend
langlangישיר
category_idcategoryIdcamelCase בצד frontend

שלב 1 — הרחבת PrayerResponse ב-schemas.py

# backend/app/schemas/schemas.py
class PrayerResponse(BaseModel):
    id: str
    slug: str
    title: str | None = None
    body: str | None = None
    seo_description: str | None = None
    seo_keywords: list[str] | None = None
    lang: str = "he"
    category_id: str | None = None
    view_count: int = 0

    model_config = ConfigDict(from_attributes=True)

שלב 2 — _localize helper ב-prayer_service.py

# backend/app/services/prayer_service.py
def _localize(prayer: PrayerModel, lang: str) -> PrayerResponse:
    """Map Prayer ORM → PrayerResponse for a given lang, with Hebrew fallback."""
    supported = ["he", "en", "fr", "ru", "es", "ar"]
    if lang not in supported:
        lang = "he"

    title   = getattr(prayer, f"title_{lang}", None)   or prayer.title_he
    body    = getattr(prayer, f"body_{lang}", None)    or prayer.body_he
    seo_d   = getattr(prayer, f"seo_description_{lang}", None) or prayer.seo_description_he
    seo_kw  = getattr(prayer, f"seo_keywords_{lang}", None)    or prayer.seo_keywords_he or []

    return PrayerResponse(
        id=str(prayer.id),
        slug=prayer.slug,
        title=title, body=body,
        seo_description=seo_d, seo_keywords=seo_kw,
        lang=lang,
        category_id=str(prayer.category_id) if prayer.category_id else None,
        view_count=prayer.view_count,
    )

שלב 3 — 4 service functions

# list_prayers
def list_prayers(db, lang="he"):
    prayers = db.query(PrayerModel).filter(PrayerModel.is_active == True).all()
    return [_localize(p, lang) for p in prayers]

# get_prayer_by_slug — מגדיל view_count!
def get_prayer_by_slug(db, slug, lang="he"):
    prayer = db.query(PrayerModel).filter(
        PrayerModel.slug == slug, PrayerModel.is_active == True
    ).first()
    if not prayer:
        raise HTTPException(status_code=404, detail="Prayer not found")
    prayer.view_count += 1
    db.commit()
    return _localize(prayer, lang)

# list_by_category
def list_by_category(db, category, lang="he"):
    prayers = (db.query(PrayerModel)
        .join(Category, PrayerModel.category_id == Category.id)
        .filter(Category.slug == category, PrayerModel.is_active == True).all())
    return [_localize(p, lang) for p in prayers]

# search_prayers
def search_prayers(db, q, lang="he"):
    title_col = getattr(PrayerModel, f"title_{lang}")
    body_col  = getattr(PrayerModel, f"body_{lang}")
    prayers = (db.query(PrayerModel)
        .filter(PrayerModel.is_active == True,
                (title_col.ilike(f"%{q}%") | body_col.ilike(f"%{q}%"))).all())
    return [_localize(p, lang) for p in prayers]

שלב 4 — Router (סדר חשוב!)

# backend/app/routers/prayers.py
# ⚠️ סדר חשוב: /search ו-/category/{cat} חייבים לפני /{slug}
# אחרת FastAPI יתפוס "search" כ-slug!

@router.get("/", response_model=list[PrayerResponse])
def list_prayers(lang: str = "he", db = Depends(get_db)):
    return prayer_service.list_prayers(db, lang)

@router.get("/search", response_model=list[PrayerResponse])
def search(q: str, lang: str = "he", db = Depends(get_db)):
    return prayer_service.search_prayers(db, q, lang)

@router.get("/category/{category}", response_model=list[PrayerResponse])
def by_category(category: str, lang: str = "he", db = Depends(get_db)):
    return prayer_service.list_by_category(db, category, lang)

@router.get("/{slug}", response_model=PrayerResponse)
def get_prayer(slug: str, lang: str = "he", db = Depends(get_db)):
    return prayer_service.get_prayer_by_slug(db, slug, lang)

✅ קריטריוני קבלה

  • GET /api/prayers?lang=he → 5 תפילות עם כל 8 שדות
  • GET /api/prayers/health?lang=hetitle: "תפילה לרפואה שלמה"
  • 2 קריאות ל-/api/prayers/healthview_count עלה ב-DB
  • GET /api/prayers/search?q=רפואה → מחזיר תפילת health
  • GET /api/prayers/category/health → מחזיר תפילת health
  • GET /api/prayers/nonexistent → 404 (לא 500)
  • Router לא מכיל לוגיקה עסקית — רק קריאה ל-service
  • ruff check . עובר ללא שגיאות
  • PR עבר review + אישור שרה
⚠️ מלכודת: getattr(PrayerModel, f"title_{lang}") (על המחלקה) מחזיר SQLAlchemy column — עובד ב-.ilike(). getattr(prayer_instance, f"title_{lang}") (על אובייקט) מחזיר string — עובד ב-_localize. לא לבלבל.
ABD-8
🟣 F4 — App Foundation: השלמת _layout.tsx
S · ~1 יום · עצמאית (מקבילה ל-F2)
עצמאית מ-F2/F3! ניתן לעבוד על זה במקביל לבניית ה-backend. גודל S — מספר שורות קוד.

❌ לפני F4

  • לא מתבצע Firebase anonymous login
  • StripeProvider לא עוטף את ה-navigator
  • useAuth() לא נקרא

✅ אחרי F4

  • Firebase Console מציג user אנונימי חדש
  • StripeProvider עוטף את השורש
  • האפליקציה נפתחת ללא קריסות

3 שינויים ב-_layout.tsx

1. הוסיפי import:

import { signInAnon } from '@/services/firebase';
import { StripeProvider } from '@stripe/stripe-react-native';
import { useAuth } from '@/hooks/useAuth';

2. הוסיפי signInAnon() ל-Promise.all:

Promise.all([
  initI18n(),
  initializeStripe(),
  signInAnon(),   // ← הוסיפי שורה זו
]).then(() => setReady(true));

3. עטפי עם StripeProvider + הוסיפי useAuth():

// בתוך function RootLayout():
useAuth(); // stub — Sprint 3 ימלא

// בתוך ה-return:
<StripeProvider publishableKey={process.env.EXPO_PUBLIC_STRIPE_PUBLISHABLE_KEY ?? ''}>
  <I18nextProvider i18n={i18n}>
    <Stack screenOptions={{ headerShown: false }} />
  </I18nextProvider>
</StripeProvider>

✅ קריטריוני קבלה

  • האפליקציה נפתחת ב-web ללא מסך שגיאה אדום
  • Firebase Console → Authentication → Users מציג user אנונימי חדש (תוך דקה)
  • npm run type-check עובר
  • npm run lint עובר
  • PR עבר review + אישור שרה
ABD-9
🟠 F5 — Home Screen: רשימת תפילות
M · ~2–3 ימים · תלוי F4 + F3 (UI = מוקדם, Integration = אחרי F3)
💡 ניתן להתחיל לפני F3! פותחות F5 עם mock data בתוך usePrayers(). ברגע שF3 נמזג — מסירות את ה-mock ועוברות ל-API אמיתי. מסירות mock לפני פתיחת PR.

❌ לפני F5

  • usePrayers() לא קיים
  • index.tsx מכיל TODO comment
  • מסך ריק / skeleton בלבד

✅ אחרי F5

  • 5 PrayerCards עם כותרות בעברית
  • LoadingSpinner בזמן loading
  • Error state בעברית אם API נופל
  • Tap על card → ניווט לעמוד תפילה

שלב 1 — usePrayers() ב-hooks/usePrayer.ts

⚠️ הוסיפי מתחת ל-stub הקיים של usePrayer(slug). אל תמחקי אותו — F6 תצטרך אותו!
// hooks/usePrayer.ts — הוסיפי אחרי ה-stub הקיים:
export function usePrayers(): {
  prayers: LocalizedPrayer[];
  isLoading: boolean;
  error: string | null;
} {
  const { lang } = useLanguageStore();
  const [prayers, setPrayers] = useState<LocalizedPrayer[]>([]);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    setIsLoading(true);
    setError(null);
    getPrayers()
      .then((data) => setPrayers(data as LocalizedPrayer[]))
      .catch((err: Error) => setError(err.message))
      .finally(() => setIsLoading(false));
  }, [lang]);

  return { prayers, isLoading, error };
}

שלב 2 — Wire ל-index.tsx

// app/(tabs)/index.tsx
export default function HomeScreen() {
  const { t } = useTranslation();
  const { prayers, isLoading, error } = usePrayers();

  if (isLoading) return <LoadingSpinner />;
  if (error)     return <Text>{t('error.loading')}</Text>;

  return (
    <View style={styles.screen}>
      <AppDownloadBanner />     {/* stub — Sprint 5 */}
      <QuickButtons />          {/* stub — Sprint 3 */}
      <FlatList
        data={prayers}
        keyExtractor={(p) => p.slug}
        renderItem={({ item }) => <PrayerCard prayer={item} />}
      />
    </View>
  );
}

שלב 3 — מפתח i18n חדש

// i18n/he.json — הוסיפי אם לא קיים:
"error": {
  "loading": "שגיאה בטעינה, נסה שוב"
}

✅ קריטריוני קבלה

  • מסך הבית מציג 5 PrayerCards עם כותרות בעברית
  • LoadingSpinner גלוי בזמן loading
  • Error state בעברית כשה-API לא רץ (לא מסך ריק)
  • Tap על card → ניווט ל-/prayer/[slug]
  • אין hardcoded עברית ב-JSX — הכל דרך t('key')
  • npm run lint + npm run type-check עוברים
  • PR עבר review + אישור שרה
ABD-10
🟢 F6 — Prayer Page + DonationWidget
M · ~2–3 ימים · תלוי F5

❌ לפני F6

  • usePrayer(slug) תמיד מחזיר null
  • DonationWidget: <Text>TODO</Text>
  • אין not-found state

✅ אחרי F6

  • כותרת + טקסט תפילה מלא מה-API
  • 5 כפתורי tier (₪18/₪36/₪72/₪180/₪360)
  • Tap על tier → highlight
  • "תפילה לא נמצאה" + כפתור חזרה

שלב 1 — מלאי את usePrayer(slug)

⚠️ אם F5 ו-F6 אצל מפתחות שונות: PR של F5 חייב להתמזג קודם — אחרת יהיה merge conflict על usePrayer.ts.
// hooks/usePrayer.ts — ממלאות את הגוף של usePrayer(slug):
export function usePrayer(slug: string) {
  const { lang } = useLanguageStore();
  const [prayer, setPrayer] = useState<LocalizedPrayer | null>(null);
  const [isLoading, setIsLoading] = useState(true);
  const [error, setError] = useState<string | null>(null);

  useEffect(() => {
    if (!slug) return;
    setIsLoading(true);
    getPrayer(slug)
      .then((data) => setPrayer(data as LocalizedPrayer))
      .catch((err: Error) => setError(err.message))
      .finally(() => setIsLoading(false));
  }, [slug, lang]);

  return { prayer, isLoading, error };
}

שלב 2 — עמוד תפילה (app/prayer/[slug].tsx)

export default function PrayerScreen() {
  const { slug } = useLocalSearchParams<{ slug: string }>();
  const { prayer, isLoading, error } = usePrayer(slug);
  const { t } = useTranslation();

  if (isLoading) return <LoadingSpinner />;

  if (!prayer || error) return (
    <View style={styles.center}>
      <Text>{t('prayer.not_found')}</Text>
      <Button label={t('common.back')} onPress={() => router.back()} />
    </View>
  );

  return (
    <View style={styles.screen}>
      <ScrollView>
        <Text style={styles.title}>{prayer.title}</Text>
        <Text style={styles.body}>{prayer.body}</Text>
      </ScrollView>
      <View style={styles.bottomBar}>
        <DonationWidget prayerId={prayer.id} />
      </View>
    </View>
  );
}

שלב 3 — DonationWidget.tsx

💡 לפני שכותבות: פתחו constants/donations.ts ווודאו את שמות השדות ב-DonationTier. פתחו components/common/Button.tsx ווודאו את שם ה-prop (label לא text לא title).
export function DonationWidget({ prayerId }: { prayerId: string }) {
  const { t } = useTranslation();
  const { currency } = useLanguageStore();
  const { selectedTier, selectTier } = useDonationStore();

  const tiers = DONATION_TIERS[currency] ?? DONATION_TIERS.ILS;

  return (
    <View style={styles.container}>
      <View style={styles.tiersRow}>
        {tiers.map((tier) => {
          const isSelected = selectedTier?.amount === tier.amount;
          return (
            <TouchableOpacity
              key={tier.amount}
              style={[styles.tierBtn, isSelected && styles.selected]}
              onPress={() => selectTier(tier)}
            >
              <Text style={[styles.tierLabel, isSelected && styles.selectedLabel]}>
                {tier.display}
              </Text>
            </TouchableOpacity>
          );
        })}
      </View>

      <Button
        label={t('donation.donate')}
        onPress={() => {
          // TODO Sprint 2: setSheetOpen(true)
        }}
      />
    </View>
  );
}

✅ קריטריוני קבלה

  • ניווט: בית → tap card → עמוד תפילה עם כותרת + גוף בעברית
  • LoadingSpinner מוצג בזמן loading
  • Slug לא תקין → "תפילה לא נמצאה" + כפתור חזרה
  • 5 כפתורי tier גלויים (₪18/₪36/₪72/₪180/₪360)
  • Tap על tier → כפתור מוהדר (highlighted), הקודם מבוטל
  • כפתור "תרום" גלוי אך ה-onPress ריק (comment: TODO Sprint 2)
  • אין hardcoded עברית — הכל דרך t('key')
  • npm run lint + npm run type-check עוברים
  • PR עבר review + אישור שרה
11

נקודות סנכרון — מתי מדברות

📋 יום 0 (לפני שמתחילות F5/F6) — 5 דקות ביחד:
עברו על טבלת הקומפוננטות המשתמשות בספרינט זה שורה אחת שורה אחת. ודאו שאף אחת לא מתכננת לכתוב מחדש קומפוננטה שכבר קיימת.
📋 יום 0 — Contract PrayerResponse ↔ LocalizedPrayer:
מפתחת F3 משתפת את ה-PrayerResponse המורחב. מפתחת F5 מאשרת שזה תואם ל-types/prayer.types.ts. 5 דקות שיחה חוסכת יום debugging.
📋 לפני שמתחילות F5 ו-F6 — קומפוננטות משותפות:
שתי המפתחות בודקות יחד: Button, LoadingSpinner, PrayerCard — מה מלא, מה stub, מי משתמשת במה.
📋 ברגע שF3 נמזג — Integration Day:
מפתחת F5 מסירה את ה-mock מ-usePrayers() ועוברת ל-API אמיתי. מריצה end-to-end test. אם הכל עובד — commits, PR.

קומפוננטות משותפות בספרינט 1 — לא כותבות מחדש!

קומפוננטהייבואמצבמי משתמשת
Button @/components/common ✅ מלאה F6 (not-found + DonationWidget)
LoadingSpinner @/components/common ⚠️ stub — עובד F5 (בית) + F6 (עמוד תפילה)
PrayerCard @/components/PrayerCard ✅ מלאה — קומפוננטת דוגמא F5 (בית)
Input @/components/common stub — לא נדרש
AppBottomSheet @/components/common stub — ספרינט 2

⚠️ נקודת מיזוג קריטית — usePrayer.ts

שתי הפונקציות יושבות באותו קובץ:
F5 מוסיפה usePrayers() — F6 ממלאת usePrayer(slug)

אם מפתחות שונות עובדות על F5 ו-F6: ה-PR של F5 חייב להתמזג לפני שמתחילות F6. אחרת merge conflict.

שער Review — כל PR

כותבת פותחת PR
המפתחת השנייה עושה review
שרה מאשרת
מיזוג ל-main
🚫 אין merge ללא אישור שרה. ללא יוצא מן הכלל.
12

ה-App בסוף הספרינט — ככה זה ייראה

מסך הבית — רשימת תפילות

┌─────────────────────────────────────┐ │ [AppDownloadBanner stub — Sprint 5]│ ├─────────────────────────────────────┤ │ [QuickButtons stub — Sprint 3] │ ├─────────────────────────────────────┤ │ │ │ ┌─────────────────────────────┐ │ │ │ 🏥 תפילה לרפואה שלמה │ │ │ │ רבונו של עולם, אתה... │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ ⭐ תפילה להצלחה │ │ │ │ ריבונו של עולם, בידך... │ │ │ └─────────────────────────────┘ │ │ │ │ ┌─────────────────────────────┐ │ │ │ 📚 תפילה לפני מבחן │ │ │ │ אנא ה׳, פתח לבי... │ │ │ └─────────────────────────────┘ │ │ │ │ [תפילה לנסיעה בטוחה...] │ │ [תפילה לברכת ילדים...] │ │ │ │ ←────────────── scroll ──────────→ │ └─────────────────────────────────────┘

עמוד תפילה + DonationWidget

┌─────────────────────────────────────┐ │ ← חזרה תפילה לרפואה שלמה │ ├─────────────────────────────────────┤ │ │ │ [ScrollView — RTL] │ │ │ │ תפילה לרפואה שלמה │ │ ───────────────────── │ │ │ │ רבונו של עולם, אתה רופא │ │ כל בשר ומפליא לעשות. │ │ בקשתי לפניך שתשלח רפואה │ │ שלמה מן השמים... │ │ │ │ [המשך גלילה...] │ │ │ ├─────────────────────────────────────┤ │ [Sticky Bottom — DonationWidget] │ │ │ │ ₪360 ₪180 [₪72] ₪36 ₪18 │ │ ← RTL row → │ │ │ │ ┌──────────────────────────────┐ │ │ │ 💙 תרום │ │ │ └──────────────────────────────┘ │ │ (onPress = כלום — TODO Sprint 2) │ └─────────────────────────────────────┘

[₪72] = highlighted (נבחר) | שאר הכפתורים = רגילים

מצב "תפילה לא נמצאה" (slug שגוי)

┌─────────────────────────────────────┐ │ │ │ תפילה לא נמצאה │ │ │ │ ┌──────────────────┐ │ │ │ ← חזרה │ │ │ └──────────────────┘ │ │ │ └─────────────────────────────────────┘

מה לא עובד — ובכוונה

פיצ'רמצב בסוף Sprint 1ספרינט
תשלום אמיתי (Stripe)כפתור "תרום" ריקSprint 2
DonationBottomSheetstub ריקSprint 2
Google/Apple LoginAnonymous בלבדSprint 3
Quick Buttons (ממש)stub מציג TextSprint 3
LanguagePicker / RTLעברית בלבדSprint 4
SEO / SSRClient-side onlySprint 5
EAS mobile buildWeb בלבדSprint 5
13

הגדרת "סיום" — ספרינט 1 הושלם כשכל זה עובד

✅ ספרינט 1 הושלם כשמישהי מהצוות יכולה לבצע את ה-flow הבא מהתחלה לסוף ללא שגיאות:

🗺️ מה מחכה בספרינטים הבאים

ספרינטנושאעיקרי
Sprint 2תשלוםStripe PaymentIntent, DonationBottomSheet, Webhook, Deploy ל-Render+Vercel
Sprint 3Auth + QuickFirebase Auth middleware, Google/Apple Sign-In, כרטיס שמור, Quick Donate, QuickButtons
Sprint 4רב-לשוניLanguagePicker, RTL/LTR, geo-IP currency, היסטוריית תרומות, Seed רב-שפתי
Sprint 5פרודקשןSSR+SEO, EAS mobile build, App Store/Play, rate limiting, AppDownloadBanner, חיפוש
Anu Banecha Donations · ספרינט 1 · 2026 · גרסה 1.0