diff --git a/GUEST_URL.md b/GUEST_URL.md
new file mode 100644
index 0000000..e2ef369
--- /dev/null
+++ b/GUEST_URL.md
@@ -0,0 +1,28 @@
+# Guest Self-Service URL
+
+Share this URL with your guests so they can update their RSVP:
+
+**http://localhost:5173/guest**
+
+## How it works:
+
+1. Guests visit the URL
+2. They enter their phone number (must match the phone number in your guest list)
+3. They can update:
+ - RSVP status (confirmed/declined/pending)
+ - Meal preference (if attending)
+ - Plus one details (if attending)
+4. Changes are immediately reflected in your admin guest list
+
+## For Production:
+
+Replace `localhost:5173` with your actual domain, e.g.:
+- `https://yourwedding.com/guest`
+- `https://weddingapp.yourname.com/guest`
+
+## Sharing Tips:
+
+- Send via text message with instructions
+- Add QR code to physical invitations
+- Include in email invitations
+- Post on wedding website
diff --git a/backend/google_contacts.py b/backend/google_contacts.py
index 237aca0..fae6b14 100644
--- a/backend/google_contacts.py
+++ b/backend/google_contacts.py
@@ -1,6 +1,40 @@
import httpx
from sqlalchemy.orm import Session
import models
+import re
+
+
+def normalize_phone_number(phone: str) -> str:
+ """
+ Convert phone numbers from +972 format to Israeli 0 format
+ Examples:
+ +972501234567 -> 0501234567
+ +972-50-123-4567 -> 0501234567
+ 972501234567 -> 0501234567
+ """
+ if not phone:
+ return phone
+
+ # Remove all non-digit characters except +
+ cleaned = re.sub(r'[^\d+]', '', phone)
+
+ # Handle +972 format
+ if cleaned.startswith('+972'):
+ # Remove +972 and add 0 prefix
+ return '0' + cleaned[4:]
+ elif cleaned.startswith('972'):
+ # Remove 972 and add 0 prefix
+ return '0' + cleaned[3:]
+
+ # If already starts with 0, return as is
+ if cleaned.startswith('0'):
+ return cleaned
+
+ # If it's a 9-digit number (Israeli mobile without prefix), add 0
+ if len(cleaned) == 9 and cleaned[0] in '5789':
+ return '0' + cleaned
+
+ return cleaned
async def import_contacts_from_google(access_token: str, db: Session, owner: str = None) -> int:
@@ -58,6 +92,10 @@ async def import_contacts_from_google(access_token: str, db: Session, owner: str
phones = connection.get("phoneNumbers", [])
phone_number = phones[0].get("value") if phones else None
+ # Normalize phone number to Israeli format (0...)
+ if phone_number:
+ phone_number = normalize_phone_number(phone_number)
+
# Check if contact already exists by email OR phone number
existing = None
if email:
diff --git a/backend/main.py b/backend/main.py
index 050e265..1e2735a 100644
--- a/backend/main.py
+++ b/backend/main.py
@@ -189,5 +189,68 @@ async def google_callback(code: str, db: Session = Depends(get_db)):
status_code=302
)
+
+# Public endpoint for guests to update their info
+@app.get("/public/guest/{phone_number}")
+def get_guest_by_phone(phone_number: str, db: Session = Depends(get_db)):
+ """
+ Public endpoint: Get guest info by phone number
+ Returns guest if found, or None to allow new registration
+ """
+ guest = db.query(models.Guest).filter(models.Guest.phone_number == phone_number).first()
+ if not guest:
+ # Return structure indicating not found, but don't raise error
+ return {"found": False, "phone_number": phone_number}
+ return {"found": True, **guest.__dict__}
+
+
+@app.put("/public/guest/{phone_number}")
+def update_guest_by_phone(
+ phone_number: str,
+ guest_update: schemas.GuestPublicUpdate,
+ db: Session = Depends(get_db)
+):
+ """
+ Public endpoint: Allow guests to update their own info using phone number
+ Creates new guest if not found (marked as 'self-service')
+ """
+ guest = db.query(models.Guest).filter(models.Guest.phone_number == phone_number).first()
+
+ if not guest:
+ # Create new guest from link (not imported from contacts)
+ guest = models.Guest(
+ first_name=guest_update.first_name or "Guest",
+ last_name=guest_update.last_name or "",
+ phone_number=phone_number,
+ rsvp_status=guest_update.rsvp_status or "pending",
+ meal_preference=guest_update.meal_preference,
+ has_plus_one=guest_update.has_plus_one or False,
+ plus_one_name=guest_update.plus_one_name,
+ owner="self-service" # Mark as self-registered via link
+ )
+ db.add(guest)
+ else:
+ # Update existing guest
+ # Always update names if provided (override contact names)
+ if guest_update.first_name is not None:
+ guest.first_name = guest_update.first_name
+ if guest_update.last_name is not None:
+ guest.last_name = guest_update.last_name
+
+ # Update other fields
+ if guest_update.rsvp_status is not None:
+ guest.rsvp_status = guest_update.rsvp_status
+ if guest_update.meal_preference is not None:
+ guest.meal_preference = guest_update.meal_preference
+ if guest_update.has_plus_one is not None:
+ guest.has_plus_one = guest_update.has_plus_one
+ if guest_update.plus_one_name is not None:
+ guest.plus_one_name = guest_update.plus_one_name
+
+ db.commit()
+ db.refresh(guest)
+ return guest
+
+
if __name__ == "__main__":
uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
\ No newline at end of file
diff --git a/backend/schemas.py b/backend/schemas.py
index 3efd92a..8eebac3 100644
--- a/backend/schemas.py
+++ b/backend/schemas.py
@@ -33,3 +33,13 @@ class Guest(GuestBase):
class Config:
from_attributes = True
+
+
+class GuestPublicUpdate(BaseModel):
+ """Schema for public guest self-service updates"""
+ first_name: Optional[str] = None
+ last_name: Optional[str] = None
+ rsvp_status: Optional[str] = None
+ meal_preference: Optional[str] = None
+ has_plus_one: Optional[bool] = None
+ plus_one_name: Optional[str] = None
diff --git a/frontend/src/App.css b/frontend/src/App.css
index 93b7719..4a52a05 100644
--- a/frontend/src/App.css
+++ b/frontend/src/App.css
@@ -1,3 +1,11 @@
+* {
+ box-sizing: border-box;
+}
+
+[dir="rtl"] {
+ text-align: right;
+}
+
.App {
min-height: 100vh;
padding: 20px;
@@ -9,6 +17,34 @@ header {
margin-bottom: 30px;
}
+.header-content {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 20px;
+ position: relative;
+}
+
+.btn-logout {
+ position: absolute;
+ left: 0;
+ padding: 8px 16px;
+ background: rgba(255, 255, 255, 0.2);
+ color: white;
+ border: 2px solid white;
+ font-size: 14px;
+}
+
+.btn-logout:hover {
+ background: rgba(255, 255, 255, 0.3);
+ transform: translateY(-1px);
+}
+
+[dir="rtl"] .btn-logout {
+ left: auto;
+ right: 0;
+}
+
header h1 {
font-size: 2.5rem;
font-weight: 600;
diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx
index b6a8838..240c401 100644
--- a/frontend/src/App.jsx
+++ b/frontend/src/App.jsx
@@ -3,6 +3,8 @@ import GuestList from './components/GuestList'
import GuestForm from './components/GuestForm'
import SearchFilter from './components/SearchFilter'
import GoogleImport from './components/GoogleImport'
+import GuestSelfService from './components/GuestSelfService'
+import Login from './components/Login'
import { getGuests, searchGuests } from './api/api'
import './App.css'
@@ -11,10 +13,30 @@ function App() {
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [editingGuest, setEditingGuest] = useState(null)
+ const [currentPage, setCurrentPage] = useState('admin') // 'admin' or 'guest'
+ const [isAuthenticated, setIsAuthenticated] = useState(false)
+
+ // Check authentication status on mount
+ useEffect(() => {
+ const authStatus = localStorage.getItem('isAuthenticated')
+ if (authStatus === 'true') {
+ setIsAuthenticated(true)
+ }
+ }, [])
+
+ // Check URL for guest mode
+ useEffect(() => {
+ const path = window.location.pathname
+ if (path === '/guest' || path === '/guest/') {
+ setCurrentPage('guest')
+ }
+ }, [])
useEffect(() => {
- loadGuests()
- }, [])
+ if (currentPage === 'admin') {
+ loadGuests()
+ }
+ }, [currentPage])
const loadGuests = async () => {
try {
@@ -59,16 +81,41 @@ function App() {
loadGuests()
}
+ const handleLogin = () => {
+ setIsAuthenticated(true)
+ }
+
+ const handleLogout = () => {
+ localStorage.removeItem('isAuthenticated')
+ setIsAuthenticated(false)
+ }
+
+ // Render guest self-service page
+ if (currentPage === 'guest') {
+ return
+ }
+
+ // Require authentication for admin panel
+ if (!isAuthenticated) {
+ return
+ }
+
+ // Render admin page
return (
-
+
@@ -76,7 +123,7 @@ function App() {
{loading ? (
-
Loading guests...
+
טוען אורחים...
) : (
{
return response.data
}
+// Public endpoints for guest self-service
+export const getGuestByPhone = async (phoneNumber) => {
+ const response = await api.get(`/public/guest/${encodeURIComponent(phoneNumber)}`)
+ return response.data
+}
+
+export const updateGuestByPhone = async (phoneNumber, guestData) => {
+ const response = await api.put(`/public/guest/${encodeURIComponent(phoneNumber)}`, guestData)
+ return response.data
+}
+
export default api
diff --git a/frontend/src/components/GoogleImport.jsx b/frontend/src/components/GoogleImport.jsx
index 0af979f..3a1e37b 100644
--- a/frontend/src/components/GoogleImport.jsx
+++ b/frontend/src/components/GoogleImport.jsx
@@ -12,14 +12,14 @@ function GoogleImport({ onImportComplete }) {
const error = urlParams.get('error')
if (imported) {
- alert(`Successfully imported ${imported} contacts from ${importOwner}'s Google account!`)
+ alert(`יובאו בהצלחה ${imported} אנשי קשר מחשבון Google של ${importOwner}!`)
onImportComplete()
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname)
}
if (error) {
- alert(`Failed to import contacts: ${error}`)
+ alert(`נכשל בייבוא אנשי הקשר: ${error}`)
// Clean up URL
window.history.replaceState({}, document.title, window.location.pathname)
}
@@ -38,7 +38,7 @@ function GoogleImport({ onImportComplete }) {
disabled={importing}
>
{importing ? (
- '⏳ Importing...'
+ '⏳ מייבא...'
) : (
<>
- Import from Google
+ ייבוא מ-Google
>
)}
diff --git a/frontend/src/components/GuestForm.jsx b/frontend/src/components/GuestForm.jsx
index 73e0906..686e205 100644
--- a/frontend/src/components/GuestForm.jsx
+++ b/frontend/src/components/GuestForm.jsx
@@ -121,12 +121,11 @@ function GuestForm({ guest, onClose }) {
value={formData.meal_preference}
onChange={handleChange}
>
-
-
-
-
-
-
+
+
+
+
+
diff --git a/frontend/src/components/GuestList.jsx b/frontend/src/components/GuestList.jsx
index e2cc5b1..ffdd127 100644
--- a/frontend/src/components/GuestList.jsx
+++ b/frontend/src/components/GuestList.jsx
@@ -32,26 +32,26 @@ function GuestList({ guests, onEdit, onUpdate }) {
const handleBulkDelete = async () => {
if (selectedGuests.length === 0) return
- if (window.confirm(`Are you sure you want to delete ${selectedGuests.length} guests?`)) {
+ if (window.confirm(`האם אתה בטוח שברצונך למחוק ${selectedGuests.length} אורחים?`)) {
try {
await deleteGuestsBulk(selectedGuests)
setSelectedGuests([])
onUpdate()
} catch (error) {
console.error('Error deleting guests:', error)
- alert('Failed to delete guests')
+ alert('נכשל במחיקת האורחים')
}
}
}
const handleDelete = async (id) => {
- if (window.confirm('Are you sure you want to delete this guest?')) {
+ if (window.confirm('האם אתה בטוח שברצונך למחוק את האורח?')) {
try {
await deleteGuest(id)
onUpdate()
} catch (error) {
console.error('Error deleting guest:', error)
- alert('Failed to delete guest')
+ alert('נכשל במחיקת האורח')
}
}
}
@@ -67,10 +67,23 @@ function GuestList({ guests, onEdit, onUpdate }) {
}
}
+ const getRsvpLabel = (status) => {
+ switch (status) {
+ case 'accepted':
+ return 'אישר'
+ case 'declined':
+ return 'סירוב'
+ case 'pending':
+ return 'המתנה'
+ default:
+ return status
+ }
+ }
+
if (guests.length === 0) {
return (
-
No guests found. Add your first guest to get started!
+
לא נמצאו אורחים. הוסף את האורח הראשון שלך!
)
}
@@ -78,10 +91,10 @@ function GuestList({ guests, onEdit, onUpdate }) {
return (
-
Guest List ({guests.length})
+
רשימת אורחים ({guests.length})
{selectedGuests.length > 0 && (
)}
@@ -112,15 +125,15 @@ function GuestList({ guests, onEdit, onUpdate }) {
checked={paginatedGuests.length > 0 && selectedGuests.length === paginatedGuests.length}
/>
-
Name |
-
Email |
-
Phone |
-
RSVP |
-
Meal |
-
Plus One |
-
Table |
-
Owner |
-
Actions |
+
שם |
+
אימייל |
+
טלפון |
+
אישור |
+
ארוחה |
+
פלאס ואן |
+
שולחן |
+
מייבא |
+
פעולות |
@@ -140,13 +153,13 @@ function GuestList({ guests, onEdit, onUpdate }) {
{guest.phone_number || '-'} |
- {guest.rsvp_status}
+ {getRsvpLabel(guest.rsvp_status)}
|
{guest.meal_preference || '-'} |
{guest.has_plus_one ? (
- ✓ {guest.plus_one_name || 'Yes'}
+ ✓ {guest.plus_one_name || 'כן'}
) : (
'-'
)}
@@ -158,13 +171,13 @@ function GuestList({ guests, onEdit, onUpdate }) {
className="btn-small btn-edit"
onClick={() => onEdit(guest)}
>
- Edit
+ ערוך
|
@@ -179,16 +192,16 @@ function GuestList({ guests, onEdit, onUpdate }) {
onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
disabled={currentPage === 1}
>
- Previous
+ הקודם
- Page {currentPage} of {totalPages}
+ עמוד {currentPage} מתוך {totalPages}
)}
diff --git a/frontend/src/components/GuestSelfService.css b/frontend/src/components/GuestSelfService.css
new file mode 100644
index 0000000..9ce435d
--- /dev/null
+++ b/frontend/src/components/GuestSelfService.css
@@ -0,0 +1,188 @@
+.guest-self-service {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.service-container {
+ background: white;
+ border-radius: 20px;
+ padding: 40px;
+ max-width: 500px;
+ width: 100%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.service-container h1 {
+ text-align: center;
+ color: #667eea;
+ margin-bottom: 10px;
+ font-size: 2.5rem;
+}
+
+.subtitle {
+ text-align: center;
+ color: #666;
+ margin-bottom: 30px;
+ font-size: 1.1rem;
+}
+
+.lookup-form,
+.update-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.form-group label {
+ font-weight: 600;
+ color: #333;
+ font-size: 0.95rem;
+}
+
+.form-group input,
+.form-group select {
+ padding: 12px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+}
+
+.form-group input:focus,
+.form-group select:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.checkbox-group {
+ flex-direction: row;
+ align-items: center;
+}
+
+.checkbox-group label {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ cursor: pointer;
+ font-weight: 500;
+}
+
+.checkbox-group input[type="checkbox"] {
+ width: 20px;
+ height: 20px;
+ cursor: pointer;
+}
+
+.btn {
+ padding: 14px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+}
+
+.btn-primary {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ width: 100%;
+}
+
+.btn-primary:hover:not(:disabled) {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
+}
+
+.btn-primary:disabled {
+ opacity: 0.6;
+ cursor: not-allowed;
+}
+
+.btn-link {
+ background: none;
+ border: none;
+ color: #667eea;
+ cursor: pointer;
+ text-decoration: underline;
+ font-size: 0.9rem;
+ padding: 0;
+ margin-top: 5px;
+}
+
+.btn-link:hover {
+ color: #764ba2;
+}
+
+.guest-info {
+ background: #f8f9ff;
+ padding: 20px;
+ border-radius: 12px;
+ margin-bottom: 20px;
+ text-align: center;
+}
+
+.guest-info h2 {
+ color: #667eea;
+ margin-bottom: 10px;
+ font-size: 1.5rem;
+}
+
+.guest-note {
+ color: #666;
+ font-size: 0.95rem;
+ margin-bottom: 10px;
+}
+
+.error-message {
+ background: #fee;
+ border: 2px solid #fcc;
+ color: #c33;
+ padding: 12px;
+ border-radius: 8px;
+ text-align: center;
+ font-weight: 500;
+}
+
+.success-message {
+ background: #efe;
+ border: 2px solid #cfc;
+ color: #3a3;
+ padding: 12px;
+ border-radius: 8px;
+ text-align: center;
+ font-weight: 500;
+ animation: slideIn 0.3s ease;
+}
+
+@keyframes slideIn {
+ from {
+ opacity: 0;
+ transform: translateY(-10px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+@media (max-width: 600px) {
+ .service-container {
+ padding: 30px 20px;
+ }
+
+ .service-container h1 {
+ font-size: 2rem;
+ }
+}
diff --git a/frontend/src/components/GuestSelfService.jsx b/frontend/src/components/GuestSelfService.jsx
new file mode 100644
index 0000000..f809861
--- /dev/null
+++ b/frontend/src/components/GuestSelfService.jsx
@@ -0,0 +1,227 @@
+import { useState } from 'react'
+import { getGuestByPhone, updateGuestByPhone } from '../api/api'
+import './GuestSelfService.css'
+
+function GuestSelfService() {
+ const [phoneNumber, setPhoneNumber] = useState('')
+ const [guest, setGuest] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState('')
+ const [success, setSuccess] = useState(false)
+ const [formData, setFormData] = useState({
+ first_name: '',
+ last_name: '',
+ rsvp_status: 'pending',
+ meal_preference: '',
+ has_plus_one: false,
+ plus_one_name: ''
+ })
+
+ const handleLookup = async (e) => {
+ e.preventDefault()
+ setError('')
+ setSuccess(false)
+ setLoading(true)
+
+ try {
+ const guestData = await getGuestByPhone(phoneNumber)
+ setGuest(guestData)
+
+ // Always start with empty form - don't show contact info
+ setFormData({
+ first_name: '',
+ last_name: '',
+ rsvp_status: 'pending',
+ meal_preference: '',
+ has_plus_one: false,
+ plus_one_name: ''
+ })
+ } catch (err) {
+ setError('Failed to check phone number. Please try again.')
+ setGuest(null)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleSubmit = async (e) => {
+ e.preventDefault()
+ setError('')
+ setSuccess(false)
+ setLoading(true)
+
+ try {
+ await updateGuestByPhone(phoneNumber, formData)
+ setSuccess(true)
+ // Refresh guest data
+ const updatedGuest = await getGuestByPhone(phoneNumber)
+ setGuest(updatedGuest)
+ } catch (err) {
+ setError('נכשל בעדכון המידע. אנא נסה שוב.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleChange = (e) => {
+ const { name, value, type, checked } = e.target
+ setFormData(prev => ({
+ ...prev,
+ [name]: type === 'checkbox' ? checked : value
+ }))
+ }
+
+ return (
+
+
+
💒 אישור הגעה לחתונה
+
עדכן את הגעתך והעדפותיך
+
+ {!guest ? (
+
+ ) : (
+
+
+
שלום! 👋
+
אנא הזן את הפרטים שלך לאישור הגעה
+
+
+
+ {success && (
+
+ ✓ המידע שלך עודכן בהצלחה!
+
+ )}
+
+ {error &&
{error}
}
+
+
+
+ )}
+
+
+ )
+}
+
+export default GuestSelfService
diff --git a/frontend/src/components/Login.css b/frontend/src/components/Login.css
new file mode 100644
index 0000000..ba14909
--- /dev/null
+++ b/frontend/src/components/Login.css
@@ -0,0 +1,126 @@
+.login-page {
+ min-height: 100vh;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 20px;
+}
+
+.login-container {
+ background: white;
+ border-radius: 20px;
+ padding: 40px;
+ max-width: 450px;
+ width: 100%;
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
+}
+
+.login-container h1 {
+ text-align: center;
+ color: #667eea;
+ margin-bottom: 10px;
+ font-size: 2rem;
+}
+
+.login-subtitle {
+ text-align: center;
+ color: #666;
+ margin-bottom: 30px;
+ font-size: 1rem;
+}
+
+.login-form {
+ display: flex;
+ flex-direction: column;
+ gap: 20px;
+}
+
+.login-form .form-group {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+}
+
+.login-form label {
+ font-weight: 600;
+ color: #333;
+ font-size: 0.95rem;
+}
+
+.login-form input {
+ padding: 12px;
+ border: 2px solid #e0e0e0;
+ border-radius: 8px;
+ font-size: 1rem;
+ transition: all 0.3s ease;
+ font-family: inherit;
+}
+
+.login-form input:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.login-form .btn {
+ padding: 14px 24px;
+ border: none;
+ border-radius: 8px;
+ font-size: 1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ margin-top: 10px;
+}
+
+.login-form .btn-primary {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ width: 100%;
+}
+
+.login-form .btn-primary:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
+}
+
+.login-form .error-message {
+ background: #fee;
+ border: 2px solid #fcc;
+ color: #c33;
+ padding: 12px;
+ border-radius: 8px;
+ text-align: center;
+ font-weight: 500;
+}
+
+.login-info {
+ margin-top: 30px;
+ padding: 20px;
+ background: #f8f9ff;
+ border-radius: 12px;
+ text-align: center;
+ font-size: 0.9rem;
+ color: #666;
+}
+
+.login-info p {
+ margin: 5px 0;
+}
+
+.login-info p:first-child {
+ font-weight: 600;
+ color: #667eea;
+ margin-bottom: 10px;
+}
+
+@media (max-width: 600px) {
+ .login-container {
+ padding: 30px 20px;
+ }
+
+ .login-container h1 {
+ font-size: 1.5rem;
+ }
+}
diff --git a/frontend/src/components/Login.jsx b/frontend/src/components/Login.jsx
new file mode 100644
index 0000000..44b8580
--- /dev/null
+++ b/frontend/src/components/Login.jsx
@@ -0,0 +1,81 @@
+import { useState } from 'react'
+import './Login.css'
+
+function Login({ onLogin }) {
+ const [credentials, setCredentials] = useState({
+ username: '',
+ password: ''
+ })
+ const [error, setError] = useState('')
+
+ const handleSubmit = (e) => {
+ e.preventDefault()
+ setError('')
+
+ // Simple authentication - you can change these credentials
+ const ADMIN_USERNAME = 'admin'
+ const ADMIN_PASSWORD = 'wedding2025'
+
+ if (credentials.username === ADMIN_USERNAME && credentials.password === ADMIN_PASSWORD) {
+ localStorage.setItem('isAuthenticated', 'true')
+ onLogin()
+ } else {
+ setError('שם משתמש או סיסמה שגויים')
+ }
+ }
+
+ const handleChange = (e) => {
+ const { name, value } = e.target
+ setCredentials(prev => ({
+ ...prev,
+ [name]: value
+ }))
+ }
+
+ return (
+
+
+
💒 כניסה לניהול רשימת מוזמנים
+
הזן שם משתמש וסיסמה לגישה
+
+
+
+
+ )
+}
+
+export default Login
diff --git a/frontend/src/components/SearchFilter.jsx b/frontend/src/components/SearchFilter.jsx
index 4ba66cc..89be8be 100644
--- a/frontend/src/components/SearchFilter.jsx
+++ b/frontend/src/components/SearchFilter.jsx
@@ -47,11 +47,11 @@ function SearchFilter({ onSearch }) {
const handleUndoImport = async () => {
if (!filters.owner) {
- alert('Please select an owner to undo their import')
+ alert('אנא בחר מייבא כדי לבטל את הייבוא שלו')
return
}
- if (window.confirm(`Are you sure you want to delete all guests imported by ${filters.owner}?`)) {
+ if (window.confirm(`האם אתה בטוח שברצונך למחוק את כל האורחים שיובאו על ידי ${filters.owner}?`)) {
try {
const result = await undoImport(filters.owner)
alert(result.message)
@@ -60,7 +60,7 @@ function SearchFilter({ onSearch }) {
onSearch({})
} catch (error) {
console.error('Error undoing import:', error)
- alert('Failed to undo import')
+ alert('נכשל בביטול הייבוא')
}
}
}
@@ -74,7 +74,7 @@ function SearchFilter({ onSearch }) {
name="query"
value={filters.query}
onChange={handleChange}
- placeholder="Search by name, email, or phone..."
+ placeholder="חיפוש לפי שם, אימייל או טלפון..."
/>
@@ -83,10 +83,10 @@ function SearchFilter({ onSearch }) {
value={filters.rsvpStatus}
onChange={handleChange}
>
-
-
-
-
+
+
+
+