umangchaudhry commited on
Commit
d828bc9
·
verified ·
1 Parent(s): ff076f1

Upload 3 files

Browse files
Files changed (3) hide show
  1. app.py +393 -831
  2. config_manager.py +65 -73
  3. google_drive_manager.py +118 -24
app.py CHANGED
@@ -190,6 +190,19 @@ st.markdown("""
190
  margin: 0.5rem 0;
191
  border-radius: 5px;
192
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
193
  </style>
194
  """, unsafe_allow_html=True)
195
 
@@ -218,6 +231,34 @@ def load_auth_config():
218
  st.error(f"Error loading authentication config: {e}")
219
  return None
220
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
221
  def get_user_folder_from_username(username):
222
  """Get the user folder based on username using wedding mappings"""
223
  try:
@@ -272,41 +313,63 @@ def get_wedding_info_for_user(username):
272
  st.error(f"Error getting wedding info for {username}: {e}")
273
  return None
274
 
275
- def main():
276
- # Initialize session state
277
- if 'config_manager' not in st.session_state:
278
- st.session_state.config_manager = ConfigManager()
279
-
280
- # Load authentication configuration
281
- if 'auth_config' not in st.session_state:
282
- st.session_state.auth_config = load_auth_config()
283
-
284
- if st.session_state.auth_config is None:
285
- st.error("Failed to load authentication configuration. Please check your Google Drive setup.")
286
- st.stop()
287
-
288
- # Create authenticator (auto_hash=True by default, so passwords will be hashed automatically)
289
- authenticator = stauth.Authenticate(
290
- st.session_state.auth_config['credentials'],
291
- st.session_state.auth_config['cookie']['name'],
292
- st.session_state.auth_config['cookie']['key'],
293
- st.session_state.auth_config['cookie']['expiry_days']
294
- )
295
-
296
- # Check authentication status
297
- if 'authentication_status' not in st.session_state:
298
- st.session_state.authentication_status = None
299
-
300
- # Check if user is already authenticated
301
- if st.session_state.get('authentication_status'):
302
- # User is authenticated, show main app
303
- show_main_app(authenticator)
304
- else:
305
- # Show login page and handle authentication
306
- show_login_page(authenticator)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
 
308
- def show_login_page(authenticator):
309
- """Show the login page"""
310
 
311
  # Hero Section
312
  st.markdown("""
@@ -319,42 +382,103 @@ def show_login_page(authenticator):
319
  margin-bottom: 3rem;
320
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
321
  ">
322
- <h1 style="font-size: 3.5rem; margin-bottom: 1rem; font-weight: 700;">💒 Wedding Planner</h1>
323
  <h2 style="font-size: 1.8rem; margin-bottom: 2rem; font-weight: 300; opacity: 0.9;">
324
- Your Complete Wedding Planning Solution
325
  </h2>
326
  <p style="font-size: 1.2rem; max-width: 600px; margin: 0 auto; line-height: 1.6;">
327
- Organize your special day with our comprehensive wedding planning tool.
328
- Manage guests, track tasks, coordinate vendors, and create unforgettable memories.
329
  </p>
330
  </div>
331
  """, unsafe_allow_html=True)
332
 
333
- # Add signup/login toggle
334
- col1, col2 = st.columns([1, 1])
335
- with col1:
336
- if st.button("🔐 Login", type="primary", use_container_width=True):
337
- st.session_state.show_signup = False
338
- st.rerun()
339
- with col2:
340
- if st.button("📝 Sign Up", use_container_width=True):
341
- st.session_state.show_signup = True
342
- st.rerun()
343
 
344
- # Show appropriate form based on state
345
- if st.session_state.get('show_signup', False):
346
- show_signup_page(authenticator)
347
- else:
348
- show_login_form(authenticator)
349
 
350
- # Show features section only if not showing signup
351
- if not st.session_state.get('show_signup', False):
352
- show_features_section()
353
-
354
- def show_login_form(authenticator):
355
- """Show the login form"""
356
- # Features Section
357
- st.markdown("## What You Can Do")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
358
 
359
  col1, col2, col3 = st.columns(3)
360
 
@@ -417,194 +541,22 @@ def show_login_form(authenticator):
417
  </ul>
418
  </div>
419
  """, unsafe_allow_html=True)
420
-
421
- # Additional Features
422
- col4, col5, col6 = st.columns(3)
423
-
424
- with col4:
425
- st.markdown("""
426
- <div style="
427
- background: #f8f9fa;
428
- padding: 2rem;
429
- border-radius: 10px;
430
- border-left: 4px solid #4a7c59;
431
- margin-bottom: 2rem;
432
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
433
- ">
434
- <h3 style="color: #2d5016; margin-bottom: 1rem;">👰 Wedding Party</h3>
435
- <ul style="color: #666; line-height: 1.8;">
436
- <li>Manage bridal party</li>
437
- <li>Track responsibilities</li>
438
- <li>Coordinate schedules</li>
439
- <li>Store contact info</li>
440
- </ul>
441
- </div>
442
- """, unsafe_allow_html=True)
443
-
444
- with col5:
445
- st.markdown("""
446
- <div style="
447
- background: #f8f9fa;
448
- padding: 2rem;
449
- border-radius: 10px;
450
- border-left: 4px solid #4a7c59;
451
- margin-bottom: 2rem;
452
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
453
- ">
454
- <h3 style="color: #2d5016; margin-bottom: 1rem;">📊 Dashboard</h3>
455
- <ul style="color: #666; line-height: 1.8;">
456
- <li>Visual progress tracking</li>
457
- <li>Key metrics overview</li>
458
- <li>Timeline management</li>
459
- <li>Quick insights</li>
460
- </ul>
461
- </div>
462
- """, unsafe_allow_html=True)
463
-
464
- with col6:
465
- st.markdown("""
466
- <div style="
467
- background: #f8f9fa;
468
- padding: 2rem;
469
- border-radius: 10px;
470
- border-left: 4px solid #4a7c59;
471
- margin-bottom: 2rem;
472
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
473
- ">
474
- <h3 style="color: #2d5016; margin-bottom: 1rem;">☁️ Cloud Sync</h3>
475
- <ul style="color: #666; line-height: 1.8;">
476
- <li>Google Drive integration</li>
477
- <li>Automatic backups</li>
478
- <li>Multi-device access</li>
479
- <li>Real-time updates</li>
480
- </ul>
481
- </div>
482
- """, unsafe_allow_html=True)
483
-
484
- st.markdown("---")
485
-
486
- # Login Section
487
- st.markdown("## 🔐 Login to Your Wedding Planner")
488
-
489
- # Login form
490
- try:
491
- authenticator.login(location='main')
492
- except Exception as e:
493
- st.error(f"Login error: {e}")
494
-
495
- # Check authentication status and show appropriate message
496
- if st.session_state.get('authentication_status') is False:
497
- st.error("❌ Invalid username or password")
498
- elif st.session_state.get('authentication_status') is None:
499
- st.info("🔐 Please enter your username and password")
500
- elif st.session_state.get('authentication_status'):
501
- st.success(f"✅ Welcome, {st.session_state.get('name', 'User')}!")
502
- st.rerun() # Refresh to show main app
503
-
504
- def show_features_section():
505
- """Show the features section"""
506
- # This function is called from show_login_form, so we don't duplicate the features here
507
- pass
508
 
509
- def show_signup_page(authenticator):
510
- """Show the signup page with wedding setup"""
511
- st.markdown("## 📝 Create Your Wedding Account")
512
-
513
- # Initialize session state for signup if not exists
514
- if 'signup_step' not in st.session_state:
515
- st.session_state.signup_step = 1 # 1: Account info, 2: Wedding setup
516
-
517
- if st.session_state.signup_step == 1:
518
- show_account_signup_form(authenticator)
519
- else:
520
- show_wedding_setup_for_signup()
521
-
522
- def show_account_signup_form(authenticator):
523
- """Show the account signup form"""
524
- st.markdown("### Step 1: Create Your Account")
525
 
526
- with st.form("account_signup"):
527
- st.markdown("#### Personal Information")
528
-
529
- col1, col2 = st.columns(2)
530
- with col1:
531
- first_name = st.text_input("First Name", placeholder="Enter your first name")
532
- last_name = st.text_input("Last Name", placeholder="Enter your last name")
533
-
534
- with col2:
535
- email = st.text_input("Email Address", placeholder="Enter your email address")
536
- username = st.text_input("Username", placeholder="Choose a username")
537
-
538
- st.markdown("#### Security")
539
- col1, col2 = st.columns(2)
540
- with col1:
541
- password = st.text_input("Password", type="password", placeholder="Choose a secure password")
542
- with col2:
543
- confirm_password = st.text_input("Confirm Password", type="password", placeholder="Confirm your password")
544
-
545
- # Terms and conditions
546
- terms_accepted = st.checkbox("I agree to the terms and conditions", value=False)
547
-
548
- form_submitted = st.form_submit_button("Create Account", type="primary")
549
-
550
- if form_submitted:
551
- # Validate form
552
- errors = []
553
-
554
- if not first_name or not last_name:
555
- errors.append("Please enter your first and last name")
556
-
557
- if not email or "@" not in email:
558
- errors.append("Please enter a valid email address")
559
-
560
- if not username or len(username) < 3:
561
- errors.append("Username must be at least 3 characters long")
562
-
563
- if not password or len(password) < 6:
564
- errors.append("Password must be at least 6 characters long")
565
-
566
- if password != confirm_password:
567
- errors.append("Passwords do not match")
568
-
569
- if not terms_accepted:
570
- errors.append("Please accept the terms and conditions")
571
-
572
- # Check if username already exists
573
- if username in st.session_state.auth_config.get('credentials', {}).get('usernames', {}):
574
- errors.append("Username already exists. Please choose a different username.")
575
-
576
- # Check if email already exists
577
- existing_emails = [user.get('email', '') for user in st.session_state.auth_config.get('credentials', {}).get('usernames', {}).values()]
578
- if email in existing_emails:
579
- errors.append("Email address already registered. Please use a different email.")
580
-
581
- if errors:
582
- for error in errors:
583
- st.error(error)
584
- else:
585
- # Store account info in session state
586
- st.session_state.signup_account_info = {
587
- 'first_name': first_name,
588
- 'last_name': last_name,
589
- 'email': email,
590
- 'username': username,
591
- 'password': password
592
- }
593
-
594
- # Move to wedding setup step
595
- st.session_state.signup_step = 2
596
- st.success("Account information saved! Now let's set up your wedding.")
597
- st.rerun()
598
-
599
- def show_wedding_setup_for_signup():
600
- """Show wedding setup form for new signups"""
601
- st.markdown("### Step 2: Set Up Your Wedding")
602
 
603
  # Initialize session state for events and form data if not exists
604
- if 'signup_events' not in st.session_state:
605
- st.session_state.signup_events = []
606
- if 'signup_form_data' not in st.session_state:
607
- st.session_state.signup_form_data = {
608
  'partner1_name': '',
609
  'partner2_name': '',
610
  'venue_city': '',
@@ -615,93 +567,111 @@ def show_wedding_setup_for_signup():
615
  }
616
 
617
  # Basic wedding information form
618
- with st.form("wedding_signup_setup"):
619
  st.markdown("#### Basic Wedding Information")
620
 
621
  col1, col2 = st.columns(2)
 
622
  with col1:
623
- partner1_name = st.text_input("Partner 1 Name", value=st.session_state.signup_form_data['partner1_name'], placeholder="Enter first partner's name")
624
- partner2_name = st.text_input("Partner 2 Name", value=st.session_state.signup_form_data['partner2_name'], placeholder="Enter second partner's name")
625
- venue_city = st.text_input("City", value=st.session_state.signup_form_data['venue_city'], placeholder="Enter city")
 
 
 
 
626
 
627
  with col2:
628
- st.markdown("**Wedding Date Range**")
629
- wedding_start_date = st.date_input("Start Date", value=st.session_state.signup_form_data['wedding_start_date'])
630
- wedding_end_date = st.date_input("End Date", value=st.session_state.signup_form_data['wedding_end_date'])
 
 
 
 
 
631
 
632
- if wedding_end_date < wedding_start_date:
633
- st.error("End date must be after start date")
634
- wedding_end_date = wedding_start_date
635
-
636
- st.markdown("#### Task Organization")
637
- st.info("Tasks will be automatically grouped by your wedding events.")
638
-
639
- st.markdown("#### Custom Tags")
640
- st.markdown("Enter custom tags (one per line):")
641
- custom_tags = st.text_area("Custom Tags", value=st.session_state.signup_form_data['custom_tags'], placeholder="e.g.,\nUrgent\nHigh Priority\nDeposit Required\nResearch Needed")
642
-
643
- st.markdown("#### Task Assignees")
644
- st.markdown("Enter people who will regularly be assigned tasks (one per line):")
645
- task_assignees = st.text_area("Task Assignees", value=st.session_state.signup_form_data['task_assignees'], placeholder="e.g.,\nMom\nDad\nWedding Planner\nBest Friend\nCoordinator")
646
-
647
- form_submitted = st.form_submit_button("Update Wedding Information")
648
-
649
- if form_submitted:
650
- # Update session state with form data
651
- st.session_state.signup_form_data = {
652
- 'partner1_name': partner1_name,
653
- 'partner2_name': partner2_name,
654
- 'venue_city': venue_city,
655
- 'wedding_start_date': wedding_start_date,
656
- 'wedding_end_date': wedding_end_date,
657
- 'custom_tags': custom_tags,
658
- 'task_assignees': task_assignees
659
- }
660
- st.success("Wedding information updated!")
661
- st.rerun()
662
 
663
- # Event management section (outside form)
664
- st.markdown("#### Wedding Events")
665
- st.markdown("Define all your wedding events with their details:")
 
 
 
 
 
 
 
 
666
 
667
- # Add/Remove event buttons
668
- col1, col2 = st.columns(2)
669
- with col1:
670
- if st.button(" Add Event"):
671
- # Set default date to wedding start date
672
- wedding_start = st.session_state.signup_form_data['wedding_start_date']
673
- st.session_state.signup_events.append({
674
- "name": "New Event",
675
- "description": "",
676
- "date_offset": 0,
677
- "requires_meal_choice": False,
678
- "meal_options": [],
679
- "location": "",
680
- "address": ""
681
- })
682
- st.rerun()
683
 
684
- with col2:
685
- if len(st.session_state.signup_events) > 0 and st.button("➖ Remove Last Event"):
686
- st.session_state.signup_events.pop()
687
- st.rerun()
688
 
689
- # Display events
690
- if st.session_state.signup_events:
691
- for i, event in enumerate(st.session_state.signup_events):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
692
  with st.expander(f"Event {i+1}: {event['name']}", expanded=True):
 
 
 
 
 
 
 
693
  col1, col2 = st.columns(2)
694
 
695
  with col1:
696
- event_name = st.text_input("Event Name", value=event['name'], key=f"signup_event_name_{i}")
697
- event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"signup_event_desc_{i}")
698
- event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"signup_event_location_{i}")
699
 
700
  with col2:
701
- # Get wedding date range
702
- wedding_start = st.session_state.signup_form_data['wedding_start_date']
703
- wedding_end = st.session_state.signup_form_data['wedding_end_date']
704
-
705
  # Calculate current event date from date_offset
706
  current_event_date = wedding_start + timedelta(days=event['date_offset'])
707
 
@@ -709,7 +679,7 @@ def show_wedding_setup_for_signup():
709
  event_date = st.date_input(
710
  "Event Date",
711
  value=current_event_date,
712
- key=f"signup_event_date_{i}",
713
  help="Select any date for this event"
714
  )
715
 
@@ -717,7 +687,7 @@ def show_wedding_setup_for_signup():
717
  if event_date < wedding_start or event_date > wedding_end:
718
  st.warning(f"⚠️ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})")
719
 
720
- requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"signup_event_meal_{i}")
721
 
722
  # Meal options section (only show if meal choice is required)
723
  if requires_meal_choice:
@@ -725,11 +695,11 @@ def show_wedding_setup_for_signup():
725
  st.markdown("Enter meal options (one per line):")
726
  current_meal_options = event.get('meal_options', [])
727
  meal_options_text = '\n'.join(current_meal_options) if current_meal_options else ''
728
- meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"signup_event_meal_options_{i}", height=100)
729
  else:
730
  meal_options = ""
731
 
732
- event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"signup_event_address_{i}", height=80)
733
 
734
  # Calculate date_offset from the selected date
735
  date_offset = (event_date - wedding_start).days
@@ -740,7 +710,7 @@ def show_wedding_setup_for_signup():
740
  meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()]
741
 
742
  # Update session state
743
- st.session_state.signup_events[i] = {
744
  "name": event_name,
745
  "description": event_description,
746
  "date_offset": date_offset,
@@ -749,172 +719,55 @@ def show_wedding_setup_for_signup():
749
  "location": event_location,
750
  "address": event_address
751
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
752
  else:
753
- st.info("No events added yet. Click 'Add Event' to get started!")
754
 
755
- # Complete signup button
756
- st.markdown("---")
757
- col1, col2, col3 = st.columns([1, 2, 1])
758
- with col2:
759
- if st.button("Complete Signup & Create Wedding", type="primary", use_container_width=True):
760
- # Validate wedding setup
761
- form_data = st.session_state.signup_form_data
762
-
763
- if not form_data['partner1_name'] or not form_data['partner2_name']:
764
- st.error("Please fill in both partner names")
765
- elif not form_data['wedding_start_date'] or not form_data['wedding_end_date']:
766
- st.error("Please set your wedding date range")
767
- else:
768
- # Create the new user and wedding
769
- if create_new_user_and_wedding():
770
- st.success("🎉 Welcome to your Wedding Planner! Your account and wedding have been created successfully.")
771
- st.info("You can now log in with your new account.")
772
-
773
- # Reset signup state
774
- for key in ['signup_step', 'signup_account_info', 'signup_events', 'signup_form_data', 'show_signup']:
775
- if key in st.session_state:
776
- del st.session_state[key]
777
-
778
- st.rerun()
779
- else:
780
- st.error("Failed to create your account. Please try again.")
781
-
782
- def create_new_user_and_wedding():
783
- """Create a new user account and wedding setup"""
784
- try:
785
- # Get account info
786
- account_info = st.session_state.signup_account_info
787
- form_data = st.session_state.signup_form_data
788
- events = st.session_state.signup_events
789
-
790
- # Generate a unique folder name for the wedding
791
- partner1_clean = ''.join(c.lower() for c in account_info['first_name'] if c.isalnum())
792
- partner2_clean = ''.join(c.lower() for c in form_data['partner2_name'] if c.isalnum())
793
- wedding_folder = f"{partner1_clean}_{partner2_clean}_wedding"
794
-
795
- # Create wedding name
796
- wedding_name = f"{form_data['partner1_name']} & {form_data['partner2_name']}'s Wedding"
797
-
798
- # Hash the password
799
- hasher = stauth.Hasher()
800
- hashed_password = hasher.hash(account_info['password'])
801
-
802
- # Create new user entry
803
- new_user = {
804
- 'email': account_info['email'],
805
- 'failed_login_attempts': 0,
806
- 'first_name': account_info['first_name'],
807
- 'last_name': account_info['last_name'],
808
- 'name': f"{account_info['first_name']} {account_info['last_name']}",
809
- 'logged_in': False,
810
- 'password': hashed_password,
811
- 'roles': ['admin', 'editor', 'viewer']
812
- }
813
-
814
- # Update auth config
815
- auth_config = st.session_state.auth_config.copy()
816
- auth_config['credentials']['usernames'][account_info['username']] = new_user
817
-
818
- # Add email to pre-authorized list
819
- if 'pre-authorized' not in auth_config:
820
- auth_config['pre-authorized'] = {'emails': []}
821
- if account_info['email'] not in auth_config['pre-authorized']['emails']:
822
- auth_config['pre-authorized']['emails'].append(account_info['email'])
823
-
824
- # Add wedding mapping
825
- if 'wedding_mappings' not in auth_config:
826
- auth_config['wedding_mappings'] = {}
827
-
828
- wedding_key = f"{partner1_clean}_{partner2_clean}"
829
- auth_config['wedding_mappings'][wedding_key] = {
830
- 'folder': wedding_folder,
831
- 'users': [account_info['username']],
832
- 'wedding_name': wedding_name
833
- }
834
-
835
- # Save updated config to Google Drive
836
- config_manager = st.session_state.config_manager
837
- if config_manager.google_drive_enabled:
838
- # Upload updated config to Google Drive
839
- config_manager.drive_manager.upload_file('config.yaml', auth_config)
840
-
841
- # Create wedding configuration
842
- custom_tags_list = [tag.strip() for tag in form_data['custom_tags'].split('\n') if tag.strip()]
843
- task_assignees_list = [assignee.strip() for assignee in form_data['task_assignees'].split('\n') if assignee.strip()]
844
-
845
- wedding_config = {
846
- 'wedding_info': {
847
- 'partner1_name': form_data['partner1_name'],
848
- 'partner2_name': form_data['partner2_name'],
849
- 'wedding_start_date': form_data['wedding_start_date'].isoformat(),
850
- 'wedding_end_date': form_data['wedding_end_date'].isoformat(),
851
- 'venue_city': form_data['venue_city']
852
- },
853
- 'custom_settings': {
854
- 'custom_tags': custom_tags_list,
855
- 'task_assignees': task_assignees_list
856
- },
857
- 'wedding_events': events
858
- }
859
-
860
- # Store wedding config in session state for immediate use
861
- # The files will be created on-demand when the user first accesses the app
862
- st.session_state[f'wedding_config_{account_info["username"]}'] = wedding_config
863
-
864
- # Create initial data files structure (will be created on first use)
865
- initial_data_files = {
866
- 'guest_list_data.json': {},
867
- 'rsvp_data.json': {},
868
- 'tasks.json': [],
869
- 'vendors.json': [],
870
- 'wedding_party.json': []
871
- }
872
-
873
- # Store initial data in session state
874
- for filename, data in initial_data_files.items():
875
- st.session_state[f'{filename}_{account_info["username"]}'] = data
876
-
877
- # Try to upload wedding config to Google Drive (this is the most important file)
878
- if config_manager.google_drive_enabled:
879
- if config_manager.drive_manager.upload_file(f'{wedding_folder}/wedding_config.json', wedding_config):
880
- st.success("✅ Wedding configuration saved to Google Drive!")
881
- else:
882
- st.warning("⚠️ Could not upload wedding config to Google Drive due to storage quota limitations. Your data is saved locally and will sync when you first use the app.")
883
 
884
- st.info("💡 **Note**: Your wedding data files will be created automatically when you first add guests, tasks, or vendors. This approach works better with Google Drive storage limitations.")
885
 
886
- # Update session state auth config
887
- st.session_state.auth_config = auth_config
 
888
 
889
- return True
 
 
890
 
891
- except Exception as e:
892
- st.error(f"Error creating user and wedding: {e}")
893
- return False
894
 
895
- def show_wedding_setup_form():
896
- """Show the wedding setup form for creating a new wedding"""
897
- st.markdown("### 📝 Create Your Wedding Configuration")
898
-
899
- # Initialize session state for events and form data if not exists
900
- if 'setup_events' not in st.session_state:
901
- st.session_state.setup_events = []
902
- if 'setup_form_data' not in st.session_state:
903
- st.session_state.setup_form_data = {
904
- 'partner1_name': '',
905
- 'partner2_name': '',
906
- 'venue_city': '',
907
- 'wedding_start_date': date.today(),
908
- 'wedding_end_date': date.today(),
909
- 'custom_tags': '',
910
- 'task_assignees': ''
911
- }
912
-
913
- # Basic wedding information form
914
- with st.form("wedding_setup"):
915
- st.markdown("#### Basic Wedding Information")
916
-
917
- col1, col2 = st.columns(2)
918
  with col1:
919
  partner1_name = st.text_input("Partner 1 Name", value=st.session_state.setup_form_data['partner1_name'], placeholder="Enter first partner's name")
920
  partner2_name = st.text_input("Partner 2 Name", value=st.session_state.setup_form_data['partner2_name'], placeholder="Enter second partner's name")
@@ -1050,7 +903,7 @@ def show_wedding_setup_form():
1050
 
1051
  # Save configuration button (after event management)
1052
  st.markdown("---")
1053
- if st.button("Save Configuration", type="primary"):
1054
  # Get form values from session state
1055
  form_data = st.session_state.setup_form_data
1056
 
@@ -1075,20 +928,64 @@ def show_wedding_setup_form():
1075
  'wedding_events': st.session_state.setup_events
1076
  }
1077
 
1078
- # Save configuration
1079
- st.session_state.config_manager.save_config(config)
1080
- # Clear setup session state
1081
- if 'setup_events' in st.session_state:
1082
- del st.session_state.setup_events
1083
- if 'setup_form_data' in st.session_state:
1084
- del st.session_state.setup_form_data
1085
- if 'show_setup_form' in st.session_state:
1086
- del st.session_state.show_setup_form
1087
- st.success("Configuration saved successfully!")
1088
- st.rerun()
 
 
 
 
 
 
 
 
 
1089
  else:
1090
  st.error("Please fill in at least the partner names and wedding date range in the form above.")
1091
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1092
 
1093
  def show_main_app(authenticator):
1094
  # Get current user from session state (set by authenticator.login)
@@ -1128,14 +1025,8 @@ def show_main_app(authenticator):
1128
  st.session_state.app_initialized = True
1129
  st.session_state.last_user_folder = user_folder
1130
  else:
1131
- # Check if this is a new user who just signed up
1132
- if 'wedding_config_' + username in st.session_state:
1133
- st.info("🎉 Welcome! Your wedding planner is ready. Your data will be saved to Google Drive when you first add content or manually sync.")
1134
- st.session_state.app_initialized = True
1135
- st.session_state.last_user_folder = user_folder
1136
- else:
1137
- st.error("Failed to load wedding data. Please try logging in again or contact support.")
1138
- return
1139
 
1140
  # Load config
1141
  config = st.session_state.config_manager.load_config()
@@ -1247,7 +1138,7 @@ def show_main_app(authenticator):
1247
  st.error("❌ Failed to save")
1248
 
1249
  # Always show pull button
1250
- if st.button("🔄 Pull Latest", help="Get latest from Google Drive", key="sidebar_pull"):
1251
  with st.spinner("Pulling latest..."):
1252
  if config_manager.manual_sync_from_drive():
1253
  st.success("✅ Latest loaded!")
@@ -1641,150 +1532,6 @@ def show_cache_management_section():
1641
  if st.button("ℹ️ Cache Info", help="Show detailed cache information"):
1642
  st.info("Cache helps improve performance by storing data in memory. Data is automatically cached when first loaded and updated when modified.")
1643
 
1644
- def show_google_drive_status_setup():
1645
- """Show Google Drive status on the setup page"""
1646
- config_manager = st.session_state.config_manager
1647
- drive_status = config_manager.get_google_drive_status()
1648
-
1649
- # Create an expander for Google Drive status
1650
- with st.expander("🔗 Google Drive Connection Status", expanded=False):
1651
- # Status display
1652
- if drive_status['enabled']:
1653
- if drive_status['status'] == 'Online':
1654
- st.success(f"✅ {drive_status['message']}")
1655
- elif drive_status['status'] == 'Offline':
1656
- st.warning(f"⚠️ {drive_status['message']}")
1657
- else:
1658
- st.error(f"❌ {drive_status['message']}")
1659
- else:
1660
- st.info(f"ℹ️ {drive_status['message']}")
1661
-
1662
- # Show files if available
1663
- if drive_status['enabled'] and 'files' in drive_status:
1664
- st.markdown("**Files found in Google Drive:**")
1665
- for file_name in drive_status['files']:
1666
- friendly_name = get_friendly_file_name(file_name)
1667
- st.markdown(f"• {friendly_name}")
1668
-
1669
- # Manual sync buttons (if enabled) - only show pull and load options
1670
- if drive_status['enabled']:
1671
- st.markdown("**Manual Sync:**")
1672
- col1, col2 = st.columns(2)
1673
-
1674
- with col1:
1675
- if st.button("📥 Sync from Google Drive", help="Download latest data from Google Drive", key="setup_sync_from"):
1676
- with st.spinner("Syncing from Google Drive..."):
1677
- if config_manager.manual_sync_from_drive():
1678
- st.success("Successfully synced from Google Drive!")
1679
- st.rerun()
1680
- else:
1681
- st.error("Failed to sync from Google Drive")
1682
-
1683
- with col2:
1684
- if st.button("🔄 Load Existing Data", help="Load existing wedding data from Google Drive", key="setup_load_existing"):
1685
- with st.spinner("Loading existing data from Google Drive..."):
1686
- if config_manager.load_existing_data_from_drive():
1687
- st.success("Successfully loaded existing data from Google Drive!")
1688
- st.info("Your wedding data has been loaded. The page will refresh to show your wedding planner.")
1689
- st.rerun()
1690
- else:
1691
- st.error("Failed to load existing data from Google Drive")
1692
-
1693
- # Configuration help
1694
- if not drive_status['enabled']:
1695
- st.markdown("**To enable Google Drive integration:**")
1696
- st.markdown("Set the following **secrets** in your Hugging Face Space settings:")
1697
- st.code("""
1698
- GOOGLE_DRIVE_FOLDER_ID=your_folder_id
1699
- GOOGLE_PROJECT_ID=your_project_id
1700
- GOOGLE_PRIVATE_KEY_ID=your_private_key_id
1701
- GOOGLE_PRIVATE_KEY=your_private_key
1702
- GOOGLE_CLIENT_EMAIL=your_client_email
1703
- GOOGLE_CLIENT_ID=your_client_id
1704
- """)
1705
-
1706
- # Debug information
1707
- st.markdown("**Debug Information:**")
1708
- folder_id = os.getenv('GOOGLE_DRIVE_FOLDER_ID')
1709
- if folder_id:
1710
- st.success(f"✅ GOOGLE_DRIVE_FOLDER_ID found: {folder_id[:10]}...")
1711
- else:
1712
- st.error("❌ GOOGLE_DRIVE_FOLDER_ID not found")
1713
-
1714
- # Check other variables
1715
- project_id = os.getenv('GOOGLE_PROJECT_ID')
1716
- client_email = os.getenv('GOOGLE_CLIENT_EMAIL')
1717
- private_key_id = os.getenv('GOOGLE_PRIVATE_KEY_ID')
1718
- private_key = os.getenv('GOOGLE_PRIVATE_KEY')
1719
- client_id = os.getenv('GOOGLE_CLIENT_ID')
1720
-
1721
- if project_id:
1722
- st.success(f"✅ GOOGLE_PROJECT_ID found: {project_id[:10]}...")
1723
- else:
1724
- st.error("❌ GOOGLE_PROJECT_ID not found")
1725
-
1726
- if client_email:
1727
- st.success(f"✅ GOOGLE_CLIENT_EMAIL found: {client_email}")
1728
- else:
1729
- st.error("❌ GOOGLE_CLIENT_EMAIL not found")
1730
-
1731
- if private_key_id:
1732
- st.success(f"✅ GOOGLE_PRIVATE_KEY_ID found: {private_key_id[:10]}...")
1733
- else:
1734
- st.error("❌ GOOGLE_PRIVATE_KEY_ID not found")
1735
-
1736
- if private_key:
1737
- st.success(f"✅ GOOGLE_PRIVATE_KEY found: {len(private_key)} characters")
1738
- else:
1739
- st.error("❌ GOOGLE_PRIVATE_KEY not found")
1740
-
1741
- if client_id:
1742
- st.success(f"✅ GOOGLE_CLIENT_ID found: {client_id[:10]}...")
1743
- else:
1744
- st.error("❌ GOOGLE_CLIENT_ID not found")
1745
-
1746
- # Check if all required variables are present
1747
- all_required = all([folder_id, project_id, private_key_id, private_key, client_email, client_id])
1748
- if all_required:
1749
- st.success("🎉 All required secrets are present!")
1750
- st.info("If Google Drive is still not working, there might be an authentication issue. Try restarting your Space.")
1751
-
1752
- # Test file writing permissions
1753
- st.markdown("**Testing file permissions:**")
1754
- try:
1755
- test_file = "/tmp/test_write.txt"
1756
- with open(test_file, 'w') as f:
1757
- f.write("test")
1758
- st.success("✅ Can write to /tmp directory")
1759
- os.remove(test_file)
1760
- except Exception as e:
1761
- st.error(f"❌ Cannot write to /tmp directory: {e}")
1762
-
1763
- try:
1764
- test_dir = "wedding_data"
1765
- os.makedirs(test_dir, exist_ok=True)
1766
- test_file = os.path.join(test_dir, "test.txt")
1767
- with open(test_file, 'w') as f:
1768
- f.write("test")
1769
- st.success("✅ Can write to wedding_data directory")
1770
- os.remove(test_file)
1771
- except Exception as e:
1772
- st.error(f"❌ Cannot write to wedding_data directory: {e}")
1773
- else:
1774
- st.warning("⚠️ Some required secrets are missing. Please add all secrets and restart your Space.")
1775
-
1776
- def get_friendly_file_name(filename):
1777
- """Convert technical file names to user-friendly names"""
1778
- friendly_names = {
1779
- 'wedding_config.json': 'Wedding Configuration',
1780
- 'guest_list_data.json': 'Guest List',
1781
- 'rsvp_data.json': 'RSVP Responses',
1782
- 'tasks.json': 'Tasks',
1783
- 'vendors.json': 'Vendors',
1784
- 'wedding_party.json': 'Wedding Party'
1785
- }
1786
- return friendly_names.get(filename, filename)
1787
-
1788
  def show_google_drive_sync_status():
1789
  """Show Google Drive sync status and push button prominently in main app"""
1790
  config_manager = st.session_state.config_manager
@@ -1825,7 +1572,7 @@ def show_google_drive_sync_status():
1825
  st.error("❌ Failed to save changes to Google Drive")
1826
 
1827
  with col3:
1828
- if st.button("🔄 Pull Latest", help="Get latest changes from Google Drive"):
1829
  with st.spinner("Pulling latest changes from Google Drive..."):
1830
  if config_manager.manual_sync_from_drive():
1831
  st.success("✅ Latest changes loaded from Google Drive!")
@@ -1876,189 +1623,4 @@ def show_google_drive_section():
1876
  # Manual sync buttons
1877
  if drive_status['enabled']:
1878
  st.markdown("#### Manual Sync")
1879
- col1, col2 = st.columns(2)
1880
-
1881
- with col1:
1882
- if st.button("📥 Sync from Google Drive", help="Download latest data from Google Drive"):
1883
- with st.spinner("Syncing from Google Drive..."):
1884
- if config_manager.manual_sync_from_drive():
1885
- st.success("Successfully synced from Google Drive!")
1886
- st.rerun()
1887
- else:
1888
- st.error("Failed to sync from Google Drive")
1889
-
1890
- with col2:
1891
- # Show different button text based on whether there are modified files
1892
- modified_files = config_manager.get_modified_files()
1893
- if modified_files:
1894
- button_text = f"📤 Sync {len(modified_files)} Modified Files to Drive"
1895
- button_help = f"Upload {len(modified_files)} modified files to Google Drive"
1896
- else:
1897
- button_text = "📤 Sync to Google Drive"
1898
- button_help = "Upload current data to Google Drive (no changes to sync)"
1899
-
1900
- if st.button(button_text, help=button_help, disabled=not modified_files):
1901
- with st.spinner("Syncing to Google Drive..."):
1902
- if config_manager.manual_sync_to_drive():
1903
- st.success("Successfully synced to Google Drive!")
1904
- st.rerun()
1905
- else:
1906
- st.error("Failed to sync to Google Drive")
1907
-
1908
- # Configuration info
1909
- st.markdown("#### Configuration")
1910
- st.markdown("To enable Google Drive integration, set the following environment variables:")
1911
- st.code("""
1912
- GOOGLE_DRIVE_FOLDER_ID=your_folder_id
1913
- GOOGLE_PROJECT_ID=your_project_id
1914
- GOOGLE_PRIVATE_KEY_ID=your_private_key_id
1915
- GOOGLE_PRIVATE_KEY=your_private_key
1916
- GOOGLE_CLIENT_EMAIL=your_client_email
1917
- GOOGLE_CLIENT_ID=your_client_id
1918
- """)
1919
-
1920
- st.markdown("**Note:** Google Drive integration is automatically enabled when running on Hugging Face Spaces with proper credentials configured.")
1921
-
1922
- def show_event_management_section(config):
1923
- st.markdown("#### Manage Wedding Events")
1924
-
1925
- # Initialize session state for event editing if not exists
1926
- if 'edit_events' not in st.session_state:
1927
- st.session_state.edit_events = config.get('wedding_events', []).copy()
1928
-
1929
- wedding_events = st.session_state.edit_events
1930
- wedding_info = config.get('wedding_info', {})
1931
-
1932
- # Get wedding dates for date calculations
1933
- wedding_start_str = wedding_info.get('wedding_start_date', '')
1934
- wedding_end_str = wedding_info.get('wedding_end_date', '')
1935
-
1936
- if not wedding_start_str or not wedding_end_str:
1937
- st.warning("Please set your wedding date range in the 'Edit Configuration' tab first.")
1938
- return
1939
-
1940
- try:
1941
- wedding_start = datetime.fromisoformat(wedding_start_str).date()
1942
- wedding_end = datetime.fromisoformat(wedding_end_str).date()
1943
- except:
1944
- st.error("Invalid wedding date format. Please check your configuration.")
1945
- return
1946
-
1947
- # Add event button
1948
- if st.button("➕ Add New Event"):
1949
- st.session_state.edit_events.append({
1950
- "name": "New Event",
1951
- "description": "",
1952
- "date_offset": 0,
1953
- "requires_meal_choice": False,
1954
- "meal_options": [],
1955
- "location": "",
1956
- "address": ""
1957
- })
1958
- st.rerun()
1959
-
1960
- # Display events for editing
1961
- if st.session_state.edit_events:
1962
- st.markdown("##### Edit Events")
1963
-
1964
- for i, event in enumerate(st.session_state.edit_events):
1965
- with st.expander(f"Event {i+1}: {event['name']}", expanded=True):
1966
- # Add delete button at the top right of each event
1967
- col_header1, col_header2 = st.columns([4, 1])
1968
- with col_header2:
1969
- if st.button("🗑️ Delete", key=f"delete_event_{i}", help="Delete this event"):
1970
- st.session_state.edit_events.pop(i)
1971
- st.rerun()
1972
-
1973
- col1, col2 = st.columns(2)
1974
-
1975
- with col1:
1976
- event_name = st.text_input("Event Name", value=event['name'], key=f"settings_event_name_{i}")
1977
- event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"settings_event_desc_{i}")
1978
- event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"settings_event_location_{i}")
1979
-
1980
- with col2:
1981
- # Calculate current event date from date_offset
1982
- current_event_date = wedding_start + timedelta(days=event['date_offset'])
1983
-
1984
- # Use date input without constraints - allow any date
1985
- event_date = st.date_input(
1986
- "Event Date",
1987
- value=current_event_date,
1988
- key=f"settings_event_date_{i}",
1989
- help="Select any date for this event"
1990
- )
1991
-
1992
- # Show warning if date is outside wedding range
1993
- if event_date < wedding_start or event_date > wedding_end:
1994
- st.warning(f"⚠️ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})")
1995
-
1996
- requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"settings_event_meal_{i}")
1997
-
1998
- # Meal options section (only show if meal choice is required)
1999
- if requires_meal_choice:
2000
- st.markdown("**Meal Options**")
2001
- st.markdown("Enter meal options (one per line):")
2002
- current_meal_options = event.get('meal_options', [])
2003
- meal_options_text = '\n'.join(current_meal_options) if current_meal_options else ''
2004
- meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"settings_event_meal_options_{i}", height=100)
2005
- else:
2006
- meal_options = ""
2007
-
2008
- event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"settings_event_address_{i}", height=80)
2009
-
2010
- # Calculate date_offset from the selected date
2011
- date_offset = (event_date - wedding_start).days
2012
-
2013
- # Parse meal options
2014
- meal_options_list = []
2015
- if requires_meal_choice and meal_options:
2016
- meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()]
2017
-
2018
- # Update session state
2019
- st.session_state.edit_events[i] = {
2020
- "name": event_name,
2021
- "description": event_description,
2022
- "date_offset": date_offset,
2023
- "requires_meal_choice": requires_meal_choice,
2024
- "meal_options": meal_options_list,
2025
- "location": event_location,
2026
- "address": event_address
2027
- }
2028
-
2029
- # Save events button
2030
- st.markdown("---")
2031
- col1, col2, col3 = st.columns([1, 1, 1])
2032
- with col2:
2033
- if st.button("💾 Save Event Changes", type="primary"):
2034
- # Update the configuration with edited events
2035
- updated_config = config.copy()
2036
- updated_config['wedding_events'] = st.session_state.edit_events
2037
-
2038
- # Save the updated configuration
2039
- st.session_state.config_manager.save_config(updated_config)
2040
- st.success("Event changes saved successfully!")
2041
- st.rerun()
2042
- else:
2043
- st.info("No events added yet. Click 'Add New Event' to get started!")
2044
-
2045
- # Event summary
2046
- if st.session_state.edit_events:
2047
- st.markdown("##### Event Summary")
2048
-
2049
- col1, col2, col3 = st.columns(3)
2050
-
2051
- with col1:
2052
- total_events = len(st.session_state.edit_events)
2053
- st.metric("Total Events", total_events)
2054
-
2055
- with col2:
2056
- meal_events = len([e for e in st.session_state.edit_events if e.get('requires_meal_choice', False)])
2057
- st.metric("Events with Meals", meal_events)
2058
-
2059
- with col3:
2060
- days_span = (wedding_end - wedding_start).days + 1
2061
- st.metric("Celebration Days", days_span)
2062
-
2063
- if __name__ == "__main__":
2064
- main()
 
190
  margin: 0.5rem 0;
191
  border-radius: 5px;
192
  }
193
+
194
+ .signup-form {
195
+ background: #f8f9fa;
196
+ padding: 2rem;
197
+ border-radius: 10px;
198
+ border: 2px solid #4a7c59;
199
+ margin: 1rem 0;
200
+ }
201
+
202
+ .auth-container {
203
+ max-width: 600px;
204
+ margin: 0 auto;
205
+ }
206
  </style>
207
  """, unsafe_allow_html=True)
208
 
 
231
  st.error(f"Error loading authentication config: {e}")
232
  return None
233
 
234
+ def save_auth_config(config):
235
+ """Save authentication configuration to Google Drive and local file"""
236
+ try:
237
+ config_manager = st.session_state.config_manager
238
+
239
+ # Convert config to YAML string
240
+ import yaml
241
+ yaml_content = yaml.dump(config, default_flow_style=False, sort_keys=False)
242
+
243
+ # Save to Google Drive if enabled
244
+ if config_manager.google_drive_enabled:
245
+ if config_manager.drive_manager.upload_file('config.yaml', yaml_content):
246
+ print("✅ Auth config saved to Google Drive")
247
+ else:
248
+ print("❌ Failed to save auth config to Google Drive")
249
+
250
+ # Also save locally as backup
251
+ with open('config.yaml', 'w') as file:
252
+ yaml.dump(config, file, default_flow_style=False, sort_keys=False)
253
+
254
+ # Update session state
255
+ st.session_state.auth_config = config
256
+ return True
257
+
258
+ except Exception as e:
259
+ st.error(f"Error saving authentication config: {e}")
260
+ return False
261
+
262
  def get_user_folder_from_username(username):
263
  """Get the user folder based on username using wedding mappings"""
264
  try:
 
313
  st.error(f"Error getting wedding info for {username}: {e}")
314
  return None
315
 
316
+ def create_new_wedding_folder(folder_name):
317
+ """Create a new wedding folder in Google Drive with initial data files"""
318
+ try:
319
+ config_manager = st.session_state.config_manager
320
+
321
+ if not config_manager.google_drive_enabled:
322
+ return False
323
+
324
+ # Create empty data files for the new wedding
325
+ initial_files = {
326
+ 'wedding_config.json': {
327
+ 'wedding_info': {
328
+ 'partner1_name': '',
329
+ 'partner2_name': '',
330
+ 'wedding_start_date': '',
331
+ 'wedding_end_date': '',
332
+ 'venue_city': ''
333
+ },
334
+ 'custom_settings': {
335
+ 'custom_tags': [
336
+ "Wedding Party", "Urgent", "Rehearsal", "Timeline",
337
+ "Maid of Honor", "Best Man", "Bridesmaid", "Groomsman",
338
+ "Flower Girl", "Ring Bearer", "Usher", "Reader",
339
+ "Venue", "Catering", "Photography", "Videography", "Music/DJ",
340
+ "Flowers", "Decor", "Attire", "Hair & Makeup", "Transportation",
341
+ "Invitations", "Cake", "Officiant", "Other",
342
+ "Decorations", "Centerpieces", "Favors", "Signage", "Linens",
343
+ "Tableware", "Lighting", "Accessories", "Stationery", "Gifts",
344
+ "Vendor", "Item", "Deposit Required", "Final Payment", "Installment",
345
+ "Food & Beverage", "Music", "Entertainment", "Lodging"
346
+ ],
347
+ 'task_assignees': []
348
+ },
349
+ 'wedding_events': []
350
+ },
351
+ 'guest_list_data.json': [],
352
+ 'rsvp_data.json': {},
353
+ 'tasks.json': [],
354
+ 'vendors.json': [],
355
+ 'wedding_party.json': []
356
+ }
357
+
358
+ # Upload each file to the new folder
359
+ for filename, content in initial_files.items():
360
+ full_path = f"{folder_name}/{filename}"
361
+ if not config_manager.drive_manager.upload_file(full_path, content):
362
+ st.error(f"Failed to create {filename} in {folder_name}")
363
+ return False
364
+
365
+ return True
366
+
367
+ except Exception as e:
368
+ st.error(f"Error creating wedding folder: {e}")
369
+ return False
370
 
371
+ def show_signup_page(authenticator):
372
+ """Show the signup page with user registration"""
373
 
374
  # Hero Section
375
  st.markdown("""
 
382
  margin-bottom: 3rem;
383
  box-shadow: 0 10px 30px rgba(0,0,0,0.2);
384
  ">
385
+ <h1 style="font-size: 3.5rem; margin-bottom: 1rem; font-weight: 700;">💒 Create Your Wedding Account</h1>
386
  <h2 style="font-size: 1.8rem; margin-bottom: 2rem; font-weight: 300; opacity: 0.9;">
387
+ Join thousands of couples planning their perfect day
388
  </h2>
389
  <p style="font-size: 1.2rem; max-width: 600px; margin: 0 auto; line-height: 1.6;">
390
+ Create your personalized wedding planning workspace with guest management,
391
+ task tracking, vendor coordination, and so much more.
392
  </p>
393
  </div>
394
  """, unsafe_allow_html=True)
395
 
396
+ # Registration Section
397
+ st.markdown('<div class="auth-container">', unsafe_allow_html=True)
 
 
 
 
 
 
 
 
398
 
399
+ # Create tabs for signup and login
400
+ tab1, tab2 = st.tabs(["Create Account", "Login"])
 
 
 
401
 
402
+ with tab1:
403
+ st.markdown("## ✨ Create Your Wedding Planning Account")
404
+
405
+ # User Registration Form
406
+ try:
407
+ email, username, name = authenticator.register_user(
408
+ location='main',
409
+ pre_authorized=None, # Allow any user to register
410
+ captcha=False, # Disable captcha for simplicity
411
+ clear_on_submit=False,
412
+ key='register_user'
413
+ )
414
+
415
+ if email:
416
+ st.success(f"✅ Account created successfully for {name}!")
417
+ st.success("🎉 Now let's set up your wedding details...")
418
+
419
+ # Update auth config and create wedding folder
420
+ auth_config = st.session_state.auth_config
421
+
422
+ if auth_config:
423
+ # Create wedding folder name (using username)
424
+ wedding_folder = username.lower().replace(' ', '_')
425
+
426
+ # Add new wedding mapping
427
+ if 'wedding_mappings' not in auth_config:
428
+ auth_config['wedding_mappings'] = {}
429
+
430
+ auth_config['wedding_mappings'][wedding_folder] = {
431
+ 'wedding_name': f"{name}'s Wedding",
432
+ 'folder': wedding_folder,
433
+ 'users': [username]
434
+ }
435
+
436
+ # Save updated auth config
437
+ if save_auth_config(auth_config):
438
+ # Create wedding folder with initial files
439
+ if create_new_wedding_folder(wedding_folder):
440
+ st.success("📁 Wedding workspace created in Google Drive!")
441
+
442
+ # Show wedding setup form
443
+ st.session_state['show_wedding_setup'] = True
444
+ st.session_state['new_user_folder'] = wedding_folder
445
+ st.rerun()
446
+ else:
447
+ st.warning("Account created but failed to create wedding workspace. You can set up your wedding details after login.")
448
+ else:
449
+ st.warning("Account created but failed to save wedding mapping. Please contact support.")
450
+
451
+ except Exception as e:
452
+ if "Username already taken" in str(e):
453
+ st.error("❌ Username already exists. Please choose a different username.")
454
+ elif "Email already taken" in str(e):
455
+ st.error("❌ Email already registered. Please use a different email or login instead.")
456
+ else:
457
+ st.error(f"Registration error: {e}")
458
+
459
+ with tab2:
460
+ st.markdown("## 🔐 Login to Your Account")
461
+
462
+ # Login form
463
+ try:
464
+ authenticator.login(location='main')
465
+ except Exception as e:
466
+ st.error(f"Login error: {e}")
467
+
468
+ # Check authentication status and show appropriate message
469
+ if st.session_state.get('authentication_status') is False:
470
+ st.error("❌ Invalid username or password")
471
+ elif st.session_state.get('authentication_status') is None:
472
+ st.info("📝 Please enter your username and password")
473
+ elif st.session_state.get('authentication_status'):
474
+ st.success(f"✅ Welcome back, {st.session_state.get('name', 'User')}!")
475
+ st.rerun() # Refresh to show main app
476
+
477
+ st.markdown('</div>', unsafe_allow_html=True)
478
+
479
+ # Features showcase (same as before)
480
+ st.markdown("---")
481
+ st.markdown("## ✨ What You Get With Your Wedding Account")
482
 
483
  col1, col2, col3 = st.columns(3)
484
 
 
541
  </ul>
542
  </div>
543
  """, unsafe_allow_html=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
544
 
545
+ def show_wedding_setup_form():
546
+ """Show the wedding setup form for creating a new wedding"""
547
+ st.markdown("### 📝 Set Up Your Wedding Details")
 
 
 
 
 
 
 
 
 
 
 
 
 
548
 
549
+ # Get the new user folder from session state
550
+ new_user_folder = st.session_state.get('new_user_folder')
551
+ if not new_user_folder:
552
+ st.error("No wedding folder found. Please try registering again.")
553
+ return
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
554
 
555
  # Initialize session state for events and form data if not exists
556
+ if 'setup_events' not in st.session_state:
557
+ st.session_state.setup_events = []
558
+ if 'setup_form_data' not in st.session_state:
559
+ st.session_state.setup_form_data = {
560
  'partner1_name': '',
561
  'partner2_name': '',
562
  'venue_city': '',
 
567
  }
568
 
569
  # Basic wedding information form
570
+ with st.form("wedding_setup"):
571
  st.markdown("#### Basic Wedding Information")
572
 
573
  col1, col2 = st.columns(2)
574
+
575
  with col1:
576
+ if st.button("📥 Sync from Google Drive", help="Download latest data from Google Drive"):
577
+ with st.spinner("Syncing from Google Drive..."):
578
+ if config_manager.manual_sync_from_drive():
579
+ st.success("Successfully synced from Google Drive!")
580
+ st.rerun()
581
+ else:
582
+ st.error("Failed to sync from Google Drive")
583
 
584
  with col2:
585
+ # Show different button text based on whether there are modified files
586
+ modified_files = config_manager.get_modified_files()
587
+ if modified_files:
588
+ button_text = f"📤 Sync {len(modified_files)} Modified Files to Drive"
589
+ button_help = f"Upload {len(modified_files)} modified files to Google Drive"
590
+ else:
591
+ button_text = "📤 Sync to Google Drive"
592
+ button_help = "Upload current data to Google Drive (no changes to sync)"
593
 
594
+ if st.button(button_text, help=button_help, disabled=not modified_files):
595
+ with st.spinner("Syncing to Google Drive..."):
596
+ if config_manager.manual_sync_to_drive():
597
+ st.success("Successfully synced to Google Drive!")
598
+ st.rerun()
599
+ else:
600
+ st.error("Failed to sync to Google Drive")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
601
 
602
+ # Configuration info
603
+ st.markdown("#### Configuration")
604
+ st.markdown("To enable Google Drive integration, set the following environment variables:")
605
+ st.code("""
606
+ GOOGLE_DRIVE_FOLDER_ID=your_folder_id
607
+ GOOGLE_PROJECT_ID=your_project_id
608
+ GOOGLE_PRIVATE_KEY_ID=your_private_key_id
609
+ GOOGLE_PRIVATE_KEY=your_private_key
610
+ GOOGLE_CLIENT_EMAIL=your_client_email
611
+ GOOGLE_CLIENT_ID=your_client_id
612
+ """)
613
 
614
+ st.markdown("**Note:** Google Drive integration is automatically enabled when running on Hugging Face Spaces with proper credentials configured.")
615
+
616
+ def show_event_management_section(config):
617
+ st.markdown("#### Manage Wedding Events")
 
 
 
 
 
 
 
 
 
 
 
 
618
 
619
+ # Initialize session state for event editing if not exists
620
+ if 'edit_events' not in st.session_state:
621
+ st.session_state.edit_events = config.get('wedding_events', []).copy()
 
622
 
623
+ wedding_events = st.session_state.edit_events
624
+ wedding_info = config.get('wedding_info', {})
625
+
626
+ # Get wedding dates for date calculations
627
+ wedding_start_str = wedding_info.get('wedding_start_date', '')
628
+ wedding_end_str = wedding_info.get('wedding_end_date', '')
629
+
630
+ if not wedding_start_str or not wedding_end_str:
631
+ st.warning("Please set your wedding date range in the 'Edit Configuration' tab first.")
632
+ return
633
+
634
+ try:
635
+ wedding_start = datetime.fromisoformat(wedding_start_str).date()
636
+ wedding_end = datetime.fromisoformat(wedding_end_str).date()
637
+ except:
638
+ st.error("Invalid wedding date format. Please check your configuration.")
639
+ return
640
+
641
+ # Add event button
642
+ if st.button("➕ Add New Event"):
643
+ st.session_state.edit_events.append({
644
+ "name": "New Event",
645
+ "description": "",
646
+ "date_offset": 0,
647
+ "requires_meal_choice": False,
648
+ "meal_options": [],
649
+ "location": "",
650
+ "address": ""
651
+ })
652
+ st.rerun()
653
+
654
+ # Display events for editing
655
+ if st.session_state.edit_events:
656
+ st.markdown("##### Edit Events")
657
+
658
+ for i, event in enumerate(st.session_state.edit_events):
659
  with st.expander(f"Event {i+1}: {event['name']}", expanded=True):
660
+ # Add delete button at the top right of each event
661
+ col_header1, col_header2 = st.columns([4, 1])
662
+ with col_header2:
663
+ if st.button("🗑️ Delete", key=f"delete_event_{i}", help="Delete this event"):
664
+ st.session_state.edit_events.pop(i)
665
+ st.rerun()
666
+
667
  col1, col2 = st.columns(2)
668
 
669
  with col1:
670
+ event_name = st.text_input("Event Name", value=event['name'], key=f"settings_event_name_{i}")
671
+ event_description = st.text_input("Time", value=event['description'], placeholder="e.g., 2:00 PM, 6:30 PM", key=f"settings_event_desc_{i}")
672
+ event_location = st.text_input("Location Name", value=event.get('location', ''), placeholder="e.g., Central Park, Grand Ballroom", key=f"settings_event_location_{i}")
673
 
674
  with col2:
 
 
 
 
675
  # Calculate current event date from date_offset
676
  current_event_date = wedding_start + timedelta(days=event['date_offset'])
677
 
 
679
  event_date = st.date_input(
680
  "Event Date",
681
  value=current_event_date,
682
+ key=f"settings_event_date_{i}",
683
  help="Select any date for this event"
684
  )
685
 
 
687
  if event_date < wedding_start or event_date > wedding_end:
688
  st.warning(f"⚠️ Selected date is outside your wedding date range ({wedding_start.strftime('%B %d, %Y')} - {wedding_end.strftime('%B %d, %Y')})")
689
 
690
+ requires_meal_choice = st.checkbox("Requires Meal Choice", value=event['requires_meal_choice'], key=f"settings_event_meal_{i}")
691
 
692
  # Meal options section (only show if meal choice is required)
693
  if requires_meal_choice:
 
695
  st.markdown("Enter meal options (one per line):")
696
  current_meal_options = event.get('meal_options', [])
697
  meal_options_text = '\n'.join(current_meal_options) if current_meal_options else ''
698
+ meal_options = st.text_area("Meal Options", value=meal_options_text, placeholder="e.g.,\nDuck\nSurf & Turf\nRisotto (vegetarian)\nStuffed Squash (vegetarian)", key=f"settings_event_meal_options_{i}", height=100)
699
  else:
700
  meal_options = ""
701
 
702
+ event_address = st.text_area("Address", value=event.get('address', ''), placeholder="Enter full address (street, city, state, zip code)", key=f"settings_event_address_{i}", height=80)
703
 
704
  # Calculate date_offset from the selected date
705
  date_offset = (event_date - wedding_start).days
 
710
  meal_options_list = [option.strip() for option in meal_options.split('\n') if option.strip()]
711
 
712
  # Update session state
713
+ st.session_state.edit_events[i] = {
714
  "name": event_name,
715
  "description": event_description,
716
  "date_offset": date_offset,
 
719
  "location": event_location,
720
  "address": event_address
721
  }
722
+
723
+ # Save events button
724
+ st.markdown("---")
725
+ col1, col2, col3 = st.columns([1, 1, 1])
726
+ with col2:
727
+ if st.button("💾 Save Event Changes", type="primary"):
728
+ # Update the configuration with edited events
729
+ updated_config = config.copy()
730
+ updated_config['wedding_events'] = st.session_state.edit_events
731
+
732
+ # Save the updated configuration
733
+ st.session_state.config_manager.save_config(updated_config)
734
+ st.success("Event changes saved successfully!")
735
+ st.rerun()
736
  else:
737
+ st.info("No events added yet. Click 'Add New Event' to get started!")
738
 
739
+ # Event summary
740
+ if st.session_state.edit_events:
741
+ st.markdown("##### Event Summary")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
742
 
743
+ col1, col2, col3 = st.columns(3)
744
 
745
+ with col1:
746
+ total_events = len(st.session_state.edit_events)
747
+ st.metric("Total Events", total_events)
748
 
749
+ with col2:
750
+ meal_events = len([e for e in st.session_state.edit_events if e.get('requires_meal_choice', False)])
751
+ st.metric("Events with Meals", meal_events)
752
 
753
+ with col3:
754
+ days_span = (wedding_end - wedding_start).days + 1
755
+ st.metric("Celebration Days", days_span)
756
 
757
+ def get_friendly_file_name(filename):
758
+ """Convert technical file names to user-friendly names"""
759
+ friendly_names = {
760
+ 'wedding_config.json': 'Wedding Configuration',
761
+ 'guest_list_data.json': 'Guest List',
762
+ 'rsvp_data.json': 'RSVP Responses',
763
+ 'tasks.json': 'Tasks',
764
+ 'vendors.json': 'Vendors',
765
+ 'wedding_party.json': 'Wedding Party'
766
+ }
767
+ return friendly_names.get(filename, filename)
768
+
769
+ if __name__ == "__main__":
770
+ main() = st.columns(2)
 
 
 
 
 
 
 
 
 
771
  with col1:
772
  partner1_name = st.text_input("Partner 1 Name", value=st.session_state.setup_form_data['partner1_name'], placeholder="Enter first partner's name")
773
  partner2_name = st.text_input("Partner 2 Name", value=st.session_state.setup_form_data['partner2_name'], placeholder="Enter second partner's name")
 
903
 
904
  # Save configuration button (after event management)
905
  st.markdown("---")
906
+ if st.button("Save Configuration & Start Planning", type="primary"):
907
  # Get form values from session state
908
  form_data = st.session_state.setup_form_data
909
 
 
928
  'wedding_events': st.session_state.setup_events
929
  }
930
 
931
+ # Save configuration to the new wedding folder
932
+ config_manager = st.session_state.config_manager
933
+ if config_manager.google_drive_enabled:
934
+ full_config_path = f"{new_user_folder}/wedding_config.json"
935
+ if config_manager.drive_manager.upload_file(full_config_path, config):
936
+ # Update user folder in config manager
937
+ config_manager.set_user_folder(new_user_folder)
938
+
939
+ # Clear setup session state
940
+ for key in ['setup_events', 'setup_form_data', 'show_wedding_setup', 'new_user_folder']:
941
+ if key in st.session_state:
942
+ del st.session_state[key]
943
+
944
+ st.success("✅ Wedding configuration saved successfully!")
945
+ st.success("🎉 Welcome to your personalized wedding planner!")
946
+ st.rerun()
947
+ else:
948
+ st.error("Failed to save wedding configuration. Please try again.")
949
+ else:
950
+ st.error("Google Drive not available. Cannot save configuration.")
951
  else:
952
  st.error("Please fill in at least the partner names and wedding date range in the form above.")
953
 
954
+ def main():
955
+ # Initialize session state
956
+ if 'config_manager' not in st.session_state:
957
+ st.session_state.config_manager = ConfigManager()
958
+
959
+ # Load authentication configuration
960
+ if 'auth_config' not in st.session_state:
961
+ st.session_state.auth_config = load_auth_config()
962
+
963
+ if st.session_state.auth_config is None:
964
+ st.error("Failed to load authentication configuration. Please check your Google Drive setup.")
965
+ st.stop()
966
+
967
+ # Create authenticator using the new v0.4.2 syntax
968
+ authenticator = stauth.Authenticate(
969
+ st.session_state.auth_config['credentials'],
970
+ st.session_state.auth_config['cookie']['name'],
971
+ st.session_state.auth_config['cookie']['key'],
972
+ st.session_state.auth_config['cookie']['expiry_days']
973
+ )
974
+
975
+ # Check if user wants to see wedding setup
976
+ if st.session_state.get('show_wedding_setup', False):
977
+ show_wedding_setup_form()
978
+ return
979
+
980
+ # Check authentication status from session state (v0.4.2 uses session state)
981
+ authentication_status = st.session_state.get('authentication_status')
982
+
983
+ if authentication_status:
984
+ # User is authenticated, show main app
985
+ show_main_app(authenticator)
986
+ else:
987
+ # Show signup/login page
988
+ show_signup_page(authenticator)
989
 
990
  def show_main_app(authenticator):
991
  # Get current user from session state (set by authenticator.login)
 
1025
  st.session_state.app_initialized = True
1026
  st.session_state.last_user_folder = user_folder
1027
  else:
1028
+ st.error("Failed to load wedding data.")
1029
+ return
 
 
 
 
 
 
1030
 
1031
  # Load config
1032
  config = st.session_state.config_manager.load_config()
 
1138
  st.error("❌ Failed to save")
1139
 
1140
  # Always show pull button
1141
+ if st.button("📥 Pull Latest", help="Get latest from Google Drive", key="sidebar_pull"):
1142
  with st.spinner("Pulling latest..."):
1143
  if config_manager.manual_sync_from_drive():
1144
  st.success("✅ Latest loaded!")
 
1532
  if st.button("ℹ️ Cache Info", help="Show detailed cache information"):
1533
  st.info("Cache helps improve performance by storing data in memory. Data is automatically cached when first loaded and updated when modified.")
1534
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1535
  def show_google_drive_sync_status():
1536
  """Show Google Drive sync status and push button prominently in main app"""
1537
  config_manager = st.session_state.config_manager
 
1572
  st.error("❌ Failed to save changes to Google Drive")
1573
 
1574
  with col3:
1575
+ if st.button("📥 Pull Latest", help="Get latest changes from Google Drive"):
1576
  with st.spinner("Pulling latest changes from Google Drive..."):
1577
  if config_manager.manual_sync_from_drive():
1578
  st.success("✅ Latest changes loaded from Google Drive!")
 
1623
  # Manual sync buttons
1624
  if drive_status['enabled']:
1625
  st.markdown("#### Manual Sync")
1626
+ col1, col2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
config_manager.py CHANGED
@@ -1,5 +1,6 @@
1
  import json
2
  import os
 
3
  from datetime import datetime
4
  from google_drive_manager import GoogleDriveManager
5
  import streamlit as st
@@ -573,85 +574,13 @@ class ConfigManager:
573
 
574
  self.data_loaded_from_drive = True
575
  return True
576
-
577
- # If no config found in Google Drive, check if this is a new user with data in session state
578
- return self.load_new_user_data_from_session()
579
-
580
  except Exception as e:
581
  print(f"Error loading existing data from Google Drive: {e}")
582
  # This is a common issue on Hugging Face Spaces due to network/SSL issues
583
  # The error is expected and the user can retry manually
584
  return False
585
 
586
- def load_new_user_data_from_session(self):
587
- """Load new user data from session state (for users who just signed up)"""
588
- try:
589
- # Get current username from session state
590
- username = st.session_state.get('username')
591
- if not username:
592
- return False
593
-
594
- # Check if we have wedding config for this user in session state
595
- config_key = f'wedding_config_{username}'
596
- if config_key in st.session_state:
597
- config_content = st.session_state[config_key]
598
-
599
- # Save config locally (in memory or file)
600
- if self.use_memory_storage:
601
- self.memory_data['wedding_config.json'] = config_content
602
- # Update cache in session state
603
- st.session_state['cached_wedding_config'] = config_content
604
- print("Stored new user wedding_config.json in memory")
605
- else:
606
- config_path = self.get_config_file_path()
607
- config_dir = os.path.dirname(config_path)
608
- if config_dir:
609
- os.makedirs(config_dir, exist_ok=True)
610
- with open(config_path, 'w') as f:
611
- json.dump(config_content, f, indent=2)
612
- # Update cache in session state
613
- st.session_state['cached_wedding_config'] = config_content
614
- print(f"Stored new user wedding_config.json in file: {config_path}")
615
-
616
- # Load data files from session state
617
- data_files = [
618
- 'guest_list_data.json',
619
- 'rsvp_data.json',
620
- 'tasks.json',
621
- 'vendors.json',
622
- 'wedding_party.json'
623
- ]
624
-
625
- for file_name in data_files:
626
- data_key = f'{file_name}_{username}'
627
- if data_key in st.session_state:
628
- content = st.session_state[data_key]
629
- if self.use_memory_storage:
630
- self.memory_data[file_name] = content
631
- # Update cache in session state
632
- cache_key = f"cached_{file_name}"
633
- st.session_state[cache_key] = content
634
- print(f"Stored new user {file_name} in memory")
635
- else:
636
- file_path = self.get_data_file_path(file_name)
637
- file_dir = os.path.dirname(file_path)
638
- if file_dir:
639
- os.makedirs(file_dir, exist_ok=True)
640
- with open(file_path, 'w') as f:
641
- json.dump(content, f, indent=2)
642
- # Update cache in session state
643
- cache_key = f"cached_{file_name}"
644
- st.session_state[cache_key] = content
645
- print(f"Stored new user {file_name} in file: {file_path}")
646
-
647
- self.data_loaded_from_drive = True
648
- return True
649
-
650
- return False
651
- except Exception as e:
652
- print(f"Error loading new user data from session: {e}")
653
- return False
654
-
655
  def load_demo_data_from_drive(self):
656
  """Load demo wedding data from Google Drive and enable demo mode"""
657
  if not self.google_drive_enabled:
@@ -859,3 +788,66 @@ class ConfigManager:
859
  except Exception as e:
860
  print(f"Error resetting app state: {e}")
861
  return False
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import json
2
  import os
3
+ import yaml
4
  from datetime import datetime
5
  from google_drive_manager import GoogleDriveManager
6
  import streamlit as st
 
574
 
575
  self.data_loaded_from_drive = True
576
  return True
577
+ return False
 
 
 
578
  except Exception as e:
579
  print(f"Error loading existing data from Google Drive: {e}")
580
  # This is a common issue on Hugging Face Spaces due to network/SSL issues
581
  # The error is expected and the user can retry manually
582
  return False
583
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
584
  def load_demo_data_from_drive(self):
585
  """Load demo wedding data from Google Drive and enable demo mode"""
586
  if not self.google_drive_enabled:
 
788
  except Exception as e:
789
  print(f"Error resetting app state: {e}")
790
  return False
791
+
792
+ def create_wedding_folder_structure(self, folder_name: str, wedding_config: dict) -> bool:
793
+ """Create a complete wedding folder structure in Google Drive with initial files"""
794
+ if not self.google_drive_enabled:
795
+ return False
796
+
797
+ try:
798
+ # Check if folder already exists
799
+ if self.drive_manager.check_folder_exists(folder_name):
800
+ print(f"Folder {folder_name} already exists")
801
+ return True
802
+
803
+ # Create the folder
804
+ folder_id = self.drive_manager.create_folder(folder_name)
805
+ if not folder_id:
806
+ return False
807
+
808
+ # Create initial data files for the new wedding
809
+ initial_files = {
810
+ 'wedding_config.json': wedding_config,
811
+ 'guest_list_data.json': [],
812
+ 'rsvp_data.json': {},
813
+ 'tasks.json': [],
814
+ 'vendors.json': [],
815
+ 'wedding_party.json': []
816
+ }
817
+
818
+ # Upload each file to the new folder
819
+ for filename, content in initial_files.items():
820
+ full_path = f"{folder_name}/{filename}"
821
+ if not self.drive_manager.upload_file(full_path, content):
822
+ print(f"Failed to create {filename} in {folder_name}")
823
+ return False
824
+
825
+ print(f"Successfully created wedding folder structure: {folder_name}")
826
+ return True
827
+
828
+ except Exception as e:
829
+ print(f"Error creating wedding folder structure: {e}")
830
+ return False
831
+
832
+ def load_auth_config(self):
833
+ """Load authentication configuration from Google Drive"""
834
+ if not self.google_drive_enabled:
835
+ return None
836
+
837
+ try:
838
+ auth_config = self.drive_manager.download_file('config.yaml')
839
+ return auth_config
840
+ except Exception as e:
841
+ print(f"Error loading auth config: {e}")
842
+ return None
843
+
844
+ def save_auth_config(self, config):
845
+ """Save authentication configuration to Google Drive"""
846
+ if not self.google_drive_enabled:
847
+ return False
848
+
849
+ try:
850
+ return self.drive_manager.upload_file('config.yaml', config)
851
+ except Exception as e:
852
+ print(f"Error saving auth config: {e}")
853
+ return False
google_drive_manager.py CHANGED
@@ -2,7 +2,8 @@ import os
2
  import json
3
  import tempfile
4
  import time
5
- from typing import Dict, List, Optional, Any
 
6
  from google.oauth2 import service_account
7
  from google.oauth2.credentials import Credentials
8
  from googleapiclient.discovery import build
@@ -129,7 +130,7 @@ class GoogleDriveManager:
129
  return []
130
 
131
  @retry_on_ssl_error(max_retries=3, delay=1)
132
- def download_file(self, file_name: str) -> Optional[Dict[str, Any]]:
133
  """Download a file from Google Drive and return its content"""
134
  if not self.service or not self.folder_id:
135
  return None
@@ -145,7 +146,7 @@ class GoogleDriveManager:
145
  folders = folder_results.get('files', [])
146
 
147
  if not folders:
148
- st.warning(f"File '{file_name}' not found in Google Drive")
149
  return None
150
 
151
  folder_id = folders[0]['id']
@@ -156,7 +157,7 @@ class GoogleDriveManager:
156
  files = results.get('files', [])
157
 
158
  if not files:
159
- st.warning(f"File '{file_name}' not found in Google Drive")
160
  return None
161
  else:
162
  # Direct file search in root folder
@@ -165,7 +166,7 @@ class GoogleDriveManager:
165
  files = results.get('files', [])
166
 
167
  if not files:
168
- st.warning(f"File '{file_name}' not found in Google Drive")
169
  return None
170
 
171
  file_id = files[0]['id']
@@ -174,15 +175,27 @@ class GoogleDriveManager:
174
  request = self.service.files().get_media(fileId=file_id)
175
  content = request.execute()
176
 
177
- # Try to parse as JSON
178
- try:
179
- return json.loads(content.decode('utf-8'))
180
- except json.JSONDecodeError:
181
- # If not JSON, return as string
182
- return content.decode('utf-8')
 
 
 
 
 
 
 
 
 
 
 
 
183
 
184
  except HttpError as e:
185
- st.error(f"Error downloading file '{file_name}': {str(e)}")
186
  return None
187
 
188
  @retry_on_ssl_error(max_retries=3, delay=1)
@@ -196,13 +209,20 @@ class GoogleDriveManager:
196
  if isinstance(content, (dict, list)):
197
  if file_name.endswith('.yaml') or file_name.endswith('.yml'):
198
  # For YAML files, convert to YAML string
199
- import yaml
200
  content_str = yaml.dump(content, default_flow_style=False, sort_keys=False)
 
201
  else:
202
  # For JSON files, convert to JSON string
203
  content_str = json.dumps(content, indent=2)
 
204
  else:
205
  content_str = str(content)
 
 
 
 
 
 
206
 
207
  # Convert string to bytes
208
  content_bytes = content_str.encode('utf-8')
@@ -211,12 +231,6 @@ class GoogleDriveManager:
211
  from io import BytesIO
212
  media_body = BytesIO(content_bytes)
213
 
214
- # Determine MIME type based on file extension
215
- if file_name.endswith('.yaml') or file_name.endswith('.yml'):
216
- mimetype = 'text/yaml'
217
- else:
218
- mimetype = 'application/json'
219
-
220
  # Handle subfolder paths like 'laraandumang/wedding_config.json'
221
  target_folder_id = self.folder_id
222
  actual_file_name = file_name
@@ -241,6 +255,7 @@ class GoogleDriveManager:
241
  fields='id'
242
  ).execute()
243
  target_folder_id = created_folder.get('id')
 
244
  else:
245
  target_folder_id = folders[0]['id']
246
 
@@ -258,6 +273,7 @@ class GoogleDriveManager:
258
  fileId=file_id,
259
  media_body=media
260
  ).execute()
 
261
  else:
262
  # Create new file
263
  file_metadata = {
@@ -270,14 +286,15 @@ class GoogleDriveManager:
270
  body=file_metadata,
271
  media_body=media
272
  ).execute()
 
273
 
274
  return True
275
 
276
  except HttpError as e:
277
- st.error(f"Error uploading file '{file_name}': {str(e)}")
278
  return False
279
  except Exception as e:
280
- st.error(f"Unexpected error uploading file '{file_name}': {str(e)}")
281
  return False
282
 
283
  def sync_from_drive(self, file_names: List[str]) -> Dict[str, Any]:
@@ -290,9 +307,14 @@ class GoogleDriveManager:
290
  synced_files[file_name] = content
291
  # Save to local temp directory
292
  local_path = os.path.join(self.temp_dir, file_name)
 
 
293
  with open(local_path, 'w') as f:
294
  if isinstance(content, (dict, list)):
295
- json.dump(content, f, indent=2)
 
 
 
296
  else:
297
  f.write(str(content))
298
 
@@ -315,7 +337,23 @@ class GoogleDriveManager:
315
  return None
316
 
317
  try:
318
- query = f"name='{file_name}' and '{self.folder_id}' in parents and trashed=false"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
319
  results = self.service.files().list(q=query).execute()
320
  files = results.get('files', [])
321
 
@@ -324,9 +362,65 @@ class GoogleDriveManager:
324
  return None
325
 
326
  except HttpError as e:
327
- st.error(f"Error getting file info for '{file_name}': {str(e)}")
328
  return None
329
 
330
  def is_online(self) -> bool:
331
  """Check if Google Drive service is available"""
332
  return self.service is not None and self.folder_id is not None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import json
3
  import tempfile
4
  import time
5
+ import yaml
6
+ from typing import Dict, List, Optional, Any, Union
7
  from google.oauth2 import service_account
8
  from google.oauth2.credentials import Credentials
9
  from googleapiclient.discovery import build
 
130
  return []
131
 
132
  @retry_on_ssl_error(max_retries=3, delay=1)
133
+ def download_file(self, file_name: str) -> Optional[Union[Dict[str, Any], str]]:
134
  """Download a file from Google Drive and return its content"""
135
  if not self.service or not self.folder_id:
136
  return None
 
146
  folders = folder_results.get('files', [])
147
 
148
  if not folders:
149
+ print(f"Folder '{folder_name}' not found in Google Drive")
150
  return None
151
 
152
  folder_id = folders[0]['id']
 
157
  files = results.get('files', [])
158
 
159
  if not files:
160
+ print(f"File '{file_name}' not found in Google Drive")
161
  return None
162
  else:
163
  # Direct file search in root folder
 
166
  files = results.get('files', [])
167
 
168
  if not files:
169
+ print(f"File '{file_name}' not found in Google Drive")
170
  return None
171
 
172
  file_id = files[0]['id']
 
175
  request = self.service.files().get_media(fileId=file_id)
176
  content = request.execute()
177
 
178
+ # Determine file type and parse accordingly
179
+ content_str = content.decode('utf-8')
180
+
181
+ if file_name.endswith('.yaml') or file_name.endswith('.yml'):
182
+ # Parse as YAML
183
+ try:
184
+ return yaml.safe_load(content_str)
185
+ except yaml.YAMLError:
186
+ return content_str
187
+ elif file_name.endswith('.json'):
188
+ # Parse as JSON
189
+ try:
190
+ return json.loads(content_str)
191
+ except json.JSONDecodeError:
192
+ return content_str
193
+ else:
194
+ # Return as string
195
+ return content_str
196
 
197
  except HttpError as e:
198
+ print(f"Error downloading file '{file_name}': {str(e)}")
199
  return None
200
 
201
  @retry_on_ssl_error(max_retries=3, delay=1)
 
209
  if isinstance(content, (dict, list)):
210
  if file_name.endswith('.yaml') or file_name.endswith('.yml'):
211
  # For YAML files, convert to YAML string
 
212
  content_str = yaml.dump(content, default_flow_style=False, sort_keys=False)
213
+ mimetype = 'text/yaml'
214
  else:
215
  # For JSON files, convert to JSON string
216
  content_str = json.dumps(content, indent=2)
217
+ mimetype = 'application/json'
218
  else:
219
  content_str = str(content)
220
+ if file_name.endswith('.yaml') or file_name.endswith('.yml'):
221
+ mimetype = 'text/yaml'
222
+ elif file_name.endswith('.json'):
223
+ mimetype = 'application/json'
224
+ else:
225
+ mimetype = 'text/plain'
226
 
227
  # Convert string to bytes
228
  content_bytes = content_str.encode('utf-8')
 
231
  from io import BytesIO
232
  media_body = BytesIO(content_bytes)
233
 
 
 
 
 
 
 
234
  # Handle subfolder paths like 'laraandumang/wedding_config.json'
235
  target_folder_id = self.folder_id
236
  actual_file_name = file_name
 
255
  fields='id'
256
  ).execute()
257
  target_folder_id = created_folder.get('id')
258
+ print(f"Created new folder: {folder_name}")
259
  else:
260
  target_folder_id = folders[0]['id']
261
 
 
273
  fileId=file_id,
274
  media_body=media
275
  ).execute()
276
+ print(f"Updated existing file: {file_name}")
277
  else:
278
  # Create new file
279
  file_metadata = {
 
286
  body=file_metadata,
287
  media_body=media
288
  ).execute()
289
+ print(f"Created new file: {file_name}")
290
 
291
  return True
292
 
293
  except HttpError as e:
294
+ print(f"Error uploading file '{file_name}': {str(e)}")
295
  return False
296
  except Exception as e:
297
+ print(f"Unexpected error uploading file '{file_name}': {str(e)}")
298
  return False
299
 
300
  def sync_from_drive(self, file_names: List[str]) -> Dict[str, Any]:
 
307
  synced_files[file_name] = content
308
  # Save to local temp directory
309
  local_path = os.path.join(self.temp_dir, file_name)
310
+ os.makedirs(os.path.dirname(local_path), exist_ok=True)
311
+
312
  with open(local_path, 'w') as f:
313
  if isinstance(content, (dict, list)):
314
+ if file_name.endswith('.yaml') or file_name.endswith('.yml'):
315
+ yaml.dump(content, f, default_flow_style=False, sort_keys=False)
316
+ else:
317
+ json.dump(content, f, indent=2)
318
  else:
319
  f.write(str(content))
320
 
 
337
  return None
338
 
339
  try:
340
+ # Handle subfolder paths
341
+ if '/' in file_name:
342
+ folder_name, actual_file_name = file_name.split('/', 1)
343
+
344
+ # Find the subfolder first
345
+ folder_query = f"name='{folder_name}' and '{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
346
+ folder_results = self.service.files().list(q=folder_query).execute()
347
+ folders = folder_results.get('files', [])
348
+
349
+ if not folders:
350
+ return None
351
+
352
+ folder_id = folders[0]['id']
353
+ query = f"name='{actual_file_name}' and '{folder_id}' in parents and trashed=false"
354
+ else:
355
+ query = f"name='{file_name}' and '{self.folder_id}' in parents and trashed=false"
356
+
357
  results = self.service.files().list(q=query).execute()
358
  files = results.get('files', [])
359
 
 
362
  return None
363
 
364
  except HttpError as e:
365
+ print(f"Error getting file info for '{file_name}': {str(e)}")
366
  return None
367
 
368
  def is_online(self) -> bool:
369
  """Check if Google Drive service is available"""
370
  return self.service is not None and self.folder_id is not None
371
+
372
+ def create_folder(self, folder_name: str) -> Optional[str]:
373
+ """Create a new folder in Google Drive and return its ID"""
374
+ if not self.service or not self.folder_id:
375
+ return None
376
+
377
+ try:
378
+ folder_metadata = {
379
+ 'name': folder_name,
380
+ 'mimeType': 'application/vnd.google-apps.folder',
381
+ 'parents': [self.folder_id]
382
+ }
383
+
384
+ created_folder = self.service.files().create(
385
+ body=folder_metadata,
386
+ fields='id'
387
+ ).execute()
388
+
389
+ folder_id = created_folder.get('id')
390
+ print(f"Created folder '{folder_name}' with ID: {folder_id}")
391
+ return folder_id
392
+
393
+ except HttpError as e:
394
+ print(f"Error creating folder '{folder_name}': {str(e)}")
395
+ return None
396
+
397
+ def list_folders(self) -> List[Dict[str, Any]]:
398
+ """List all folders in the Google Drive directory"""
399
+ if not self.service or not self.folder_id:
400
+ return []
401
+
402
+ try:
403
+ query = f"'{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
404
+ results = self.service.files().list(
405
+ q=query,
406
+ fields="files(id, name, modifiedTime)"
407
+ ).execute()
408
+
409
+ return results.get('files', [])
410
+ except HttpError as e:
411
+ print(f"Error listing folders: {str(e)}")
412
+ return []
413
+
414
+ def check_folder_exists(self, folder_name: str) -> bool:
415
+ """Check if a folder exists in Google Drive"""
416
+ if not self.service or not self.folder_id:
417
+ return False
418
+
419
+ try:
420
+ query = f"name='{folder_name}' and '{self.folder_id}' in parents and trashed=false and mimeType='application/vnd.google-apps.folder'"
421
+ results = self.service.files().list(q=query).execute()
422
+ files = results.get('files', [])
423
+ return len(files) > 0
424
+ except HttpError as e:
425
+ print(f"Error checking folder existence: {str(e)}")
426
+ return False