Use ids instead of uuids for primary keys in family tables

This commit is contained in:
Eugene Burmakin 2025-09-30 18:43:26 +02:00
parent 698198db4b
commit db8d886ee2
20 changed files with 738 additions and 1802 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-chart-column-icon lucide-chart-column"><path d="M3 3v16a2 2 0 0 0 2 2h16"/><path d="M18 17V9"/><path d="M13 17V5"/><path d="M8 17v-3"/></svg>

After

Width:  |  Height:  |  Size: 344 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-heart-icon lucide-heart"><path d="M2 9.5a5.5 5.5 0 0 1 9.591-3.676.56.56 0 0 0 .818 0A5.49 5.49 0 0 1 22 9.5c0 2.29-1.5 4-3 5.5l-5.492 5.313a2 2 0 0 1-3 .019L5 15c-1.5-1.5-3-3.2-3-5.5"/></svg>

After

Width:  |  Height:  |  Size: 395 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-shield-check-icon lucide-shield-check"><path d="M20 13c0 5-3.5 7.5-7.66 8.95a1 1 0 0 1-.67-.01C7.5 20.5 4 18 4 13V6a1 1 0 0 1 1-1c2 0 4.5-1.2 6.24-2.72a1.17 1.17 0 0 1 1.52 0C14.51 3.81 17 5 19 5a1 1 0 0 1 1 1z"/><path d="m9 12 2 2 4-4"/></svg>

After

Width:  |  Height:  |  Size: 447 B

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-users-icon lucide-users"><path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"/><path d="M16 3.128a4 4 0 0 1 0 7.744"/><path d="M22 21v-2a4 4 0 0 0-3-3.87"/><circle cx="9" cy="7" r="4"/></svg>

After

Width:  |  Height:  |  Size: 393 B

View file

@ -70,7 +70,7 @@ class ApplicationController < ActionController::Base
end
def user_not_authorized
redirect_back fallback_location: root_path,
redirect_to (request.referer || root_path),
alert: 'You are not authorized to perform this action.',
status: :see_other
end

View file

@ -40,8 +40,8 @@ class FamiliesController < ApplicationController
# Handle validation errors
if service.errors.any?
service.errors.each do |attribute, message|
@family.errors.add(attribute, message)
service.errors.each do |error|
@family.errors.add(error.attribute, error.message)
end
end

View file

@ -23,12 +23,8 @@ class FamilyInvitationsController < ApplicationController
redirect_to root_path, alert: 'This invitation is no longer valid.' and return
end
# If user is not authenticated, redirect to registration with invitation token
unless user_signed_in?
redirect_to new_user_registration_path(invitation_token: @invitation.token) and return
end
# User is authenticated and invitation is valid - proceed with normal flow
# Show the invitation landing page regardless of authentication status
# The view will handle showing appropriate actions based on whether user is logged in
end
def create
@ -56,7 +52,7 @@ class FamilyInvitationsController < ApplicationController
end
if @invitation.expired?
redirect_to root_path, alert: 'This invitation has expired' and return
redirect_to root_path, alert: 'This invitation is no longer valid or has expired' and return
end
if @invitation.email != current_user.email
@ -70,7 +66,7 @@ class FamilyInvitationsController < ApplicationController
if service.call
redirect_to family_path(current_user.reload.family),
notice: "Welcome to #{@invitation.family.name}! You're now part of the family."
notice: 'Welcome to the family!'
else
redirect_to root_path, alert: service.error_message || 'Unable to accept invitation'
end
@ -84,7 +80,7 @@ class FamilyInvitationsController < ApplicationController
if @invitation.update(status: :cancelled)
redirect_to family_path(@family),
notice: "Invitation to #{@invitation.email} has been cancelled"
notice: 'Invitation cancelled'
else
redirect_to family_path(@family),
alert: 'Failed to cancel invitation. Please try again'

View file

@ -0,0 +1,35 @@
# frozen_string_literal: true
class Users::SessionsController < Devise::SessionsController
before_action :load_invitation_context, only: [:new]
def new
super
end
protected
def after_sign_in_path_for(resource)
# If there's an invitation token, redirect to the invitation page
if invitation_token.present?
invitation = FamilyInvitation.find_by(token: invitation_token)
if invitation&.can_be_accepted?
return family_invitation_path(invitation.token)
end
end
super(resource)
end
private
def load_invitation_context
return unless invitation_token.present?
@invitation = FamilyInvitation.find_by(token: invitation_token)
end
def invitation_token
@invitation_token ||= params[:invitation_token] || session[:invitation_token]
end
end

View file

@ -80,6 +80,8 @@ export default class extends Controller {
return;
}
const bounds = [];
this.familyMemberLocationsValue.forEach((location) => {
if (!location || !location.latitude || !location.longitude) {
return;
@ -101,12 +103,39 @@ export default class extends Controller {
// Format timestamp for display
const lastSeen = new Date(location.updated_at).toLocaleString();
// Create popup content with theme-aware styling
const popupContent = this.createPopupContent(location, lastSeen);
familyMarker.bindPopup(popupContent);
this.familyMarkersLayer.addLayer(familyMarker);
// Create small tooltip that shows automatically
const tooltipContent = this.createTooltipContent(lastSeen);
familyMarker.bindTooltip(tooltipContent, {
permanent: true,
direction: 'top',
offset: [0, -12],
className: 'family-member-tooltip'
});
// Create detailed popup that shows on click
const popupContent = this.createPopupContent(location, lastSeen);
familyMarker.bindPopup(popupContent);
this.familyMarkersLayer.addLayer(familyMarker);
// Add to bounds array for auto-zoom
bounds.push([location.latitude, location.longitude]);
});
// Store bounds for later use
this.familyMemberBounds = bounds;
}
createTooltipContent(lastSeen) {
const isDark = this.userThemeValue === 'dark';
const bgColor = isDark ? 'rgba(31, 41, 55, 0.95)' : 'rgba(255, 255, 255, 0.95)';
const textColor = isDark ? '#f9fafb' : '#111827';
return `
<div style="background-color: ${bgColor}; color: ${textColor}; padding: 4px 8px; border-radius: 4px; font-size: 11px; white-space: nowrap; box-shadow: 0 2px 4px rgba(0,0,0,0.2);">
Last updated: ${lastSeen}
</div>
`;
}
createPopupContent(location, lastSeen) {
@ -118,19 +147,20 @@ export default class extends Controller {
const emailInitial = location.email_initial || location.email?.charAt(0)?.toUpperCase() || '?';
return `
<div class="family-member-popup" style="background-color: ${bgColor}; color: ${textColor}; padding: 8px; border-radius: 6px; min-width: 200px;">
<h3 style="margin: 0 0 8px 0; color: #10B981; font-size: 14px; font-weight: bold; display: flex; align-items: center; gap: 6px;">
<span style="background-color: #10B981; color: white; border-radius: 50%; width: 20px; height: 20px; display: flex; align-items: center; justify-content: center; font-size: 12px; font-weight: bold;">${emailInitial}</span>
<div class="family-member-popup" style="background-color: ${bgColor}; color: ${textColor}; padding: 12px; border-radius: 8px; min-width: 220px;">
<h3 style="margin: 0 0 12px 0; color: #10B981; font-size: 15px; font-weight: bold; display: flex; align-items: center; gap: 8px;">
<span style="background-color: #10B981; color: white; border-radius: 50%; width: 24px; height: 24px; display: flex; align-items: center; justify-content: center; font-size: 14px; font-weight: bold;">${emailInitial}</span>
Family Member
</h3>
<p style="margin: 0 0 4px 0; font-size: 12px;">
<p style="margin: 0 0 8px 0; font-size: 13px;">
<strong>Email:</strong> ${location.email || 'Unknown'}
</p>
<p style="margin: 0 0 4px 0; font-size: 12px;">
<strong>Location:</strong> ${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}
<p style="margin: 0 0 8px 0; font-size: 13px;">
<strong>Coordinates:</strong><br/>
${location.latitude.toFixed(6)}, ${location.longitude.toFixed(6)}
</p>
<p style="margin: 0; font-size: 12px; color: ${mutedColor};">
<strong>Last seen:</strong> ${lastSeen}
<p style="margin: 0; font-size: 12px; color: ${mutedColor}; padding-top: 8px; border-top: 1px solid ${isDark ? '#374151' : '#e5e7eb'};">
<strong>Last updated:</strong> ${lastSeen}
</p>
</div>
`;
@ -178,9 +208,12 @@ export default class extends Controller {
// Listen for when the Family Members layer is added
this.map.on('overlayadd', (event) => {
if (event.name === 'Family Members' && event.layer === this.familyMarkersLayer) {
console.log('Family Members layer enabled - refreshing locations');
console.log('Family Members layer enabled - refreshing locations and zooming to fit');
this.refreshFamilyLocations();
// Zoom to show all family members
this.zoomToFitAllMembers();
// Set up periodic refresh while layer is active
this.startPeriodicRefresh();
}
@ -195,6 +228,25 @@ export default class extends Controller {
});
}
zoomToFitAllMembers() {
if (!this.familyMemberBounds || this.familyMemberBounds.length === 0) {
return;
}
// If there's only one member, center on them with a reasonable zoom
if (this.familyMemberBounds.length === 1) {
this.map.setView(this.familyMemberBounds[0], 13);
return;
}
// For multiple members, fit bounds to show all of them
const bounds = L.latLngBounds(this.familyMemberBounds);
this.map.fitBounds(bounds, {
padding: [50, 50], // Add padding around the edges
maxZoom: 15 // Don't zoom in too close
});
}
startPeriodicRefresh() {
// Clear any existing refresh interval
this.stopPeriodicRefresh();

View file

@ -1094,7 +1094,15 @@ export default class extends BaseController {
const TogglePanelControl = L.Control.extend({
onAdd: function(map) {
const button = L.DomUtil.create('button', 'toggle-panel-button');
button.innerHTML = '📅';
button.innerHTML = `
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 2v4" />
<path d="M16 2v4" />
<path d="M21 14V6a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h8" />
<path d="M3 10h18" />
<path d="m16 20 2 2 4-4" />
</svg>
`;
// Style the button with theme-aware styling
applyThemeToButton(button, controller.userTheme);
@ -1102,9 +1110,9 @@ export default class extends BaseController {
button.style.height = '48px';
button.style.borderRadius = '4px';
button.style.padding = '0';
button.style.lineHeight = '48px';
button.style.fontSize = '18px';
button.style.textAlign = 'center';
button.style.display = 'flex';
button.style.alignItems = 'center';
button.style.justifyContent = 'center';
// Disable map interactions when clicking the button
L.DomEvent.disableClickPropagation(button);

View file

@ -1,42 +1,62 @@
<div class="hero min-h-content bg-base-200">
<div class="hero min-h-content bg-base-200 dark:bg-gray-900">
<div class="hero-content flex-col lg:flex-row-reverse w-full my-10">
<div class="text-center lg:text-left">
<h1 class="text-5xl font-bold">Login now</h1>
<p class="py-6">and take control over your location data.</p>
<% if @invitation %>
<h1 class="text-5xl font-bold text-gray-900 dark:text-gray-100">Sign in to join <%= @invitation.family.name %>!</h1>
<p class="py-6 text-gray-700 dark:text-gray-300">
You've been invited by <strong><%= @invitation.invited_by.email %></strong> to join their family.
Sign in to your account to accept the invitation.
</p>
<div class="bg-blue-50 dark:bg-blue-900/30 border border-blue-200 dark:border-blue-700 rounded-lg p-4">
<p class="text-sm text-blue-800 dark:text-blue-200">
Don't have an account yet?
<%= link_to "Create one here", new_user_registration_path(invitation_token: @invitation.token), class: "font-semibold underline" %>
</p>
</div>
<% else %>
<h1 class="text-5xl font-bold text-gray-900 dark:text-gray-100">Login now</h1>
<p class="py-6 text-gray-700 dark:text-gray-300">and take control over your location data.</p>
<% if ENV['DEMO_ENV'] == 'true' %>
<p class="py-6">
<p class="py-6 text-gray-700 dark:text-gray-300">
Demo account: <strong class="text-success">demo@dawarich.app</strong> / password: <strong class="text-success">password</strong>
</p>
<% end %>
<% end %>
</div>
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 px-5 py-5">
<div class="card flex-shrink-0 w-full max-w-sm shadow-2xl bg-base-100 dark:bg-gray-800 px-5 py-5">
<%= form_for(resource, as: resource_name, url: session_path(resource_name), class: 'form-body', html: { data: { turbo: session[:dawarich_client] == 'ios' ? false : true } }) do |f| %>
<% if @invitation %>
<%= hidden_field_tag :invitation_token, params[:invitation_token] %>
<% end %>
<div class="form-control">
<%= f.label :email, class: 'label' do %>
<span class="label-text">Email</span>
<span class="label-text text-gray-900 dark:text-gray-100">Email</span>
<% end %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'input input-bordered' %>
<%= f.email_field :email, autofocus: true, autocomplete: "email", class: 'input input-bordered bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600' %>
</div>
<div class="form-control">
<%= f.label :password, class: 'label' do %>
<span class="label-text">Password</span>
<span class="label-text text-gray-900 dark:text-gray-100">Password</span>
<% end %>
<%= f.password_field :password, autocomplete: "current-password", class: 'input input-bordered' %>
<%= f.password_field :password, autocomplete: "current-password", class: 'input input-bordered bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 border-gray-300 dark:border-gray-600' %>
<% if devise_mapping.rememberable? %>
<div class="form-control">
<label class="label cursor-pointer">
<span class="label-text">Remember me</span>
<span class="label-text text-gray-900 dark:text-gray-100">Remember me</span>
<%= f.check_box :remember_me, class: 'checkbox checkbox-sm' %>
</label>
</div>
<% end %>
</div>
<div class="form-control mt-6">
<%= f.submit "Log in", class: 'btn btn-primary' %>
<%= f.submit (@invitation ? "Sign in & Accept Invitation" : "Log in"), class: 'btn btn-primary bg-blue-600 hover:bg-blue-700 dark:bg-blue-500 dark:hover:bg-blue-600 text-white border-none' %>
</div>
<% unless @invitation %>
<%= render "devise/shared/links" %>
<% end %>
<% end %>
</div>
</div>
</div>

View file

@ -1,91 +1,120 @@
<div class="container mx-auto px-4 py-8">
<div class="max-w-2xl mx-auto">
<div class="bg-white shadow rounded-lg p-8 text-center">
<!-- Family Invitation Header -->
<div class="mb-8">
<div class="mx-auto flex items-center justify-center h-16 w-16 rounded-full bg-blue-100 mb-4">
<svg class="h-8 w-8 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
</svg>
<div class="min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 dark:from-gray-900 dark:via-gray-800 dark:to-gray-900 py-12 px-4 mx-auto">
<div class="max-w-4xl mx-auto">
<!-- Hero Section -->
<div class="text-center mb-12">
<div class="mx-auto flex items-center justify-center h-24 w-24 rounded-full bg-blue-500 mb-6 shadow-xl">
<%= icon 'users', class: "h-12 w-12 text-white" %>
</div>
<h1 class="text-3xl font-bold text-gray-900 mb-2">
<%= t('family_invitations.show.title', default: 'You\'re Invited!') %>
<h1 class="text-5xl font-bold text-gray-900 dark:text-white mb-4">
Join <%= @invitation.family.name %>!
</h1>
<p class="text-xl text-gray-600 mb-4">
<%= t('family_invitations.show.invitation_message',
default: 'You have been invited to join %{family_name}',
family_name: @invitation.family.name) %>
<p class="text-xl text-gray-700 dark:text-gray-200 mb-2">
You've been invited by <strong class="text-gray-900 dark:text-white"><%= @invitation.invited_by.email %></strong> to join their family. Create your account to accept the invitation and start sharing location data.
</p>
<div class="inline-flex items-center bg-blue-100 dark:bg-blue-900/40 border border-blue-300 dark:border-blue-600 rounded-full px-4 py-2 mt-4">
<%= icon 'info', class: "h-5 w-5 text-blue-600 dark:text-blue-400 mr-2" %>
<span class="text-sm font-medium text-blue-900 dark:text-blue-100">
Your email (<%= @invitation.email %>) will be used for this account
</span>
</div>
</div>
<!-- Benefits Section -->
<div class="bg-white dark:bg-gray-800 shadow-xl rounded-2xl p-8 mb-8">
<h2 class="text-2xl font-bold text-gray-900 dark:text-white mb-6 text-center">
What benefits does joining a family bring?
</h2>
<div class="grid md:grid-cols-2 gap-6 mb-8">
<div class="flex items-start space-x-4 p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg border border-blue-100 dark:border-blue-700">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-blue-600 dark:bg-blue-500 flex items-center justify-center">
<%= icon 'map-pin', class: "h-6 w-6 text-white" %>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">
Share Location Data
</h3>
<p class="text-sm text-gray-700 dark:text-gray-200">
Share your location history with family members and see where they are
</p>
</div>
</div>
<div class="flex items-start space-x-4 p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg border border-purple-100 dark:border-purple-700">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-purple-600 dark:bg-purple-500 flex items-center justify-center">
<%= icon 'chart-column', class: "h-6 w-6 text-white" %>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">
Track your location history
</h3>
<p class="text-sm text-gray-700 dark:text-gray-200">
Access interactive maps and personal travel statistics
</p>
</div>
</div>
<div class="flex items-start space-x-4 p-4 bg-green-50 dark:bg-green-900/20 rounded-lg border border-green-100 dark:border-green-700">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-green-600 dark:bg-green-500 flex items-center justify-center">
<%= icon 'heart', class: "h-6 w-6 text-white" %>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">
Stay Connected
</h3>
<p class="text-sm text-gray-700 dark:text-gray-200">
Keep track of your loved ones' travels and adventures in real-time
</p>
</div>
</div>
<div class="flex items-start space-x-4 p-4 bg-yellow-50 dark:bg-yellow-900/20 rounded-lg border border-yellow-100 dark:border-yellow-700">
<div class="flex-shrink-0">
<div class="h-10 w-10 rounded-full bg-yellow-600 dark:bg-yellow-500 flex items-center justify-center">
<%= icon 'shield-check', class: "h-6 w-6 text-white" %>
</div>
</div>
<div>
<h3 class="font-semibold text-gray-900 dark:text-white mb-1">
Full Control & Privacy
</h3>
<p class="text-sm text-gray-700 dark:text-gray-200">
You control what and how long you share and can leave the family anytime
</p>
</div>
</div>
</div>
<!-- Invitation Details -->
<div class="bg-gray-50 rounded-lg p-6 mb-8">
<dl class="grid grid-cols-1 gap-4 sm:grid-cols-2">
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-6 mb-6 border border-gray-200 dark:border-gray-600">
<h3 class="text-sm font-medium text-gray-600 dark:text-gray-300 mb-3">Invitation Details</h3>
<div class="grid grid-cols-2 gap-4 text-sm">
<div>
<dt class="text-sm font-medium text-gray-500">
<%= t('family_invitations.show.family_name', default: 'Family Name') %>
</dt>
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.family.name %></dd>
<span class="text-gray-600 dark:text-gray-300">Family:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white"><%= @invitation.family.name %></span>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
<%= t('family_invitations.show.invited_by', default: 'Invited by') %>
</dt>
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.invited_by.email %></dd>
<span class="text-gray-600 dark:text-gray-300">Invited by:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white"><%= @invitation.invited_by.email %></span>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
<%= t('family_invitations.show.invited_email', default: 'Invited Email') %>
</dt>
<dd class="text-lg font-semibold text-gray-900"><%= @invitation.email %></dd>
<span class="text-gray-600 dark:text-gray-300">Your email:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white"><%= @invitation.email %></span>
</div>
<div>
<dt class="text-sm font-medium text-gray-500">
<%= t('family_invitations.show.expires_at', default: 'Expires') %>
</dt>
<dd class="text-lg font-semibold text-gray-900">
<%= @invitation.expires_at.strftime('%B %d, %Y at %I:%M %p') %>
</dd>
<span class="text-gray-600 dark:text-gray-300">Expires:</span>
<span class="ml-2 font-semibold text-gray-900 dark:text-white"><%= @invitation.expires_at.strftime('%b %d, %Y') %></span>
</div>
</dl>
</div>
<!-- What This Means -->
<div class="bg-blue-50 rounded-lg p-6 mb-8 text-left">
<h3 class="text-lg font-medium text-blue-900 mb-3">
<%= t('family_invitations.show.what_this_means', default: 'What does joining a family mean?') %>
</h3>
<ul class="text-sm text-blue-800 space-y-2">
<li class="flex items-start">
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<%= t('family_invitations.show.benefit_1', default: 'Share your location data with family members') %>
</li>
<li class="flex items-start">
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<%= t('family_invitations.show.benefit_2', default: 'View shared maps and statistics') %>
</li>
<li class="flex items-start">
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<%= t('family_invitations.show.benefit_3', default: 'Stay connected with loved ones\' travels') %>
</li>
<li class="flex items-start">
<svg class="flex-shrink-0 h-5 w-5 text-blue-600 mt-0.5 mr-2" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd" />
</svg>
<%= t('family_invitations.show.benefit_4', default: 'You can leave the family at any time') %>
</li>
</ul>
</div>
<!-- Action Buttons -->
@ -94,45 +123,39 @@
<!-- User is logged in, show accept button -->
<%= link_to accept_family_invitation_path(@invitation.family, @invitation),
method: :post,
class: "w-full bg-green-600 hover:bg-green-700 text-white px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
<%= t('family_invitations.show.accept_invitation', default: 'Accept Invitation') %>
class: "block w-full bg-green-600 hover:bg-green-700 text-white px-8 py-4 rounded-lg font-semibold text-lg transition-all duration-200 shadow-lg hover:shadow-xl text-center" do %>
✓ Accept Invitation & Join Family
<% end %>
<p class="text-sm text-gray-500">
<%= t('family_invitations.show.logged_in_as', default: 'Logged in as %{email}', email: current_user.email) %>
|
<%= link_to destroy_user_session_path, method: :delete, class: "text-blue-600 hover:text-blue-800" do %>
<%= t('family_invitations.show.logout', default: 'Logout') %>
<p class="text-sm text-gray-600 dark:text-gray-300 text-center">
Logged in as <%= current_user.email %>
·
<%= link_to destroy_user_session_path, method: :delete, class: "text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300" do %>
Logout
<% end %>
</p>
<% else %>
<!-- User is not logged in, show login/register options -->
<div class="space-y-3">
<%= link_to new_user_session_path,
class: "w-full bg-blue-600 hover:bg-blue-700 text-white px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
<%= t('family_invitations.show.login_to_accept', default: 'Login to Accept Invitation') %>
<!-- User is not logged in, show register button prominently -->
<%= link_to new_user_registration_path(invitation_token: @invitation.token),
class: "block w-full bg-blue-600 hover:bg-blue-700 text-white px-8 py-4 rounded-lg font-semibold text-lg transition-all duration-200 shadow-lg hover:shadow-xl text-center" do %>
Create Account & Join Family →
<% end %>
<% unless DawarichSettings.self_hosted? %>
<%= link_to new_user_registration_path,
class: "w-full bg-gray-200 hover:bg-gray-300 text-gray-800 px-8 py-3 rounded-md font-medium text-lg transition-colors duration-200 inline-block" do %>
<%= t('family_invitations.show.register_to_accept', default: 'Create Account to Accept') %>
<% end %>
<div class="text-center">
<p class="text-sm text-gray-600 dark:text-gray-300 mb-2">
Already have an account?
</p>
<%= link_to new_user_session_path(invitation_token: @invitation.token),
class: "inline-block text-blue-600 dark:text-blue-400 hover:text-blue-800 dark:hover:text-blue-300 font-medium" do %>
Sign in to accept invitation
<% end %>
</div>
<p class="text-sm text-gray-500">
<%= t('family_invitations.show.need_account', default: 'You need to be logged in to accept this invitation.') %>
</p>
<% end %>
<!-- Decline Option -->
<div class="pt-4 border-t border-gray-200">
<p class="text-sm text-gray-500 mb-2">
<%= t('family_invitations.show.decline_message', default: 'Don\'t want to join this family?') %>
</p>
<p class="text-xs text-gray-400">
<%= t('family_invitations.show.decline_instructions', default: 'You can simply close this page. The invitation will remain valid until it expires.') %>
<div class="pt-6 border-t border-gray-200 dark:border-gray-600 text-center">
<p class="text-sm text-gray-600 dark:text-gray-300">
Not interested? You can simply close this page.
</p>
</div>
</div>

View file

@ -108,7 +108,8 @@ Rails.application.routes.draw do
get 'auth/ios/success', to: 'auth/ios#success', as: :ios_success
devise_for :users, controllers: {
registrations: 'users/registrations'
registrations: 'users/registrations',
sessions: 'users/sessions'
}
resources :metrics, only: [:index]

View file

@ -2,7 +2,7 @@
class CreateFamilies < ActiveRecord::Migration[8.0]
def change
create_table :families, id: :uuid do |t|
create_table :families do |t|
t.string :name, null: false, limit: 50
t.bigint :creator_id, null: false
t.timestamps

View file

@ -2,8 +2,8 @@
class CreateFamilyMemberships < ActiveRecord::Migration[8.0]
def change
create_table :family_memberships, id: :uuid do |t|
t.uuid :family_id, null: false
create_table :family_memberships do |t|
t.bigint :family_id, null: false
t.bigint :user_id, null: false
t.integer :role, null: false, default: 1 # member
t.timestamps

View file

@ -2,8 +2,8 @@
class CreateFamilyInvitations < ActiveRecord::Migration[8.0]
def change
create_table :family_invitations, id: :uuid do |t|
t.uuid :family_id, null: false
create_table :family_invitations do |t|
t.bigint :family_id, null: false
t.string :email, null: false
t.string :token, null: false
t.datetime :expires_at, null: false

View file

@ -0,0 +1,56 @@
class ConvertFamilyTablesToBigint < ActiveRecord::Migration[8.0]
def up
# Drop dependent tables first
drop_table :family_invitations if table_exists?(:family_invitations)
drop_table :family_memberships if table_exists?(:family_memberships)
drop_table :families if table_exists?(:families)
# Recreate families table with bigint
create_table :families do |t|
t.string :name, null: false, limit: 50
t.bigint :creator_id, null: false
t.timestamps
end
add_foreign_key :families, :users, column: :creator_id, validate: false
add_index :families, :creator_id
# Recreate family_memberships table with bigint
create_table :family_memberships do |t|
t.bigint :family_id, null: false
t.bigint :user_id, null: false
t.integer :role, null: false, default: 1 # member
t.timestamps
end
add_foreign_key :family_memberships, :families, validate: false
add_foreign_key :family_memberships, :users, validate: false
add_index :family_memberships, :family_id
add_index :family_memberships, :user_id, unique: true # One family per user
add_index :family_memberships, %i[family_id role]
# Recreate family_invitations table with bigint
create_table :family_invitations do |t|
t.bigint :family_id, null: false
t.string :email, null: false
t.string :token, null: false
t.datetime :expires_at, null: false
t.bigint :invited_by_id, null: false
t.integer :status, null: false, default: 0 # pending
t.timestamps
end
add_foreign_key :family_invitations, :families, validate: false
add_foreign_key :family_invitations, :users, column: :invited_by_id, validate: false
add_index :family_invitations, :family_id
add_index :family_invitations, :email
add_index :family_invitations, :token, unique: true
add_index :family_invitations, :status
add_index :family_invitations, :expires_at
end
def down
# This migration is irreversible since we're changing primary key types
raise ActiveRecord::IrreversibleMigration
end
end

25
db/schema.rb generated
View file

@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[8.0].define(version: 2025_09_28_000001) do
ActiveRecord::Schema[8.0].define(version: 2025_09_30_150256) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
enable_extension "postgis"
@ -96,7 +96,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_000001) do
t.index ["user_id"], name: "index_exports_on_user_id"
end
create_table "families", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
create_table "families", force: :cascade do |t|
t.string "name", limit: 50, null: false
t.bigint "creator_id", null: false
t.datetime "created_at", null: false
@ -104,8 +104,8 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_000001) do
t.index ["creator_id"], name: "index_families_on_creator_id"
end
create_table "family_invitations", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "family_id", null: false
create_table "family_invitations", force: :cascade do |t|
t.bigint "family_id", null: false
t.string "email", null: false
t.string "token", null: false
t.datetime "expires_at", null: false
@ -115,20 +115,17 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_000001) do
t.datetime "updated_at", null: false
t.index ["email"], name: "index_family_invitations_on_email"
t.index ["expires_at"], name: "index_family_invitations_on_expires_at"
t.index ["family_id", "status", "expires_at"], name: "index_family_invitations_on_family_status_expires"
t.index ["family_id"], name: "index_family_invitations_on_family_id"
t.index ["status", "expires_at"], name: "index_family_invitations_on_status_and_expires_at"
t.index ["status"], name: "index_family_invitations_on_status"
t.index ["token"], name: "index_family_invitations_on_token", unique: true
end
create_table "family_memberships", id: :uuid, default: -> { "gen_random_uuid()" }, force: :cascade do |t|
t.uuid "family_id", null: false
create_table "family_memberships", force: :cascade do |t|
t.bigint "family_id", null: false
t.bigint "user_id", null: false
t.integer "role", default: 1, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["family_id", "role"], name: "index_family_memberships_on_family_and_role"
t.index ["family_id", "role"], name: "index_family_memberships_on_family_id_and_role"
t.index ["family_id"], name: "index_family_memberships_on_family_id"
t.index ["user_id"], name: "index_family_memberships_on_user_id", unique: true
@ -345,11 +342,11 @@ ActiveRecord::Schema[8.0].define(version: 2025_09_28_000001) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "areas", "users"
add_foreign_key "families", "users", column: "creator_id"
add_foreign_key "family_invitations", "families"
add_foreign_key "family_invitations", "users", column: "invited_by_id"
add_foreign_key "family_memberships", "families"
add_foreign_key "family_memberships", "users"
add_foreign_key "families", "users", column: "creator_id", validate: false
add_foreign_key "family_invitations", "families", validate: false
add_foreign_key "family_invitations", "users", column: "invited_by_id", validate: false
add_foreign_key "family_memberships", "families", validate: false
add_foreign_key "family_memberships", "users", validate: false
add_foreign_key "notifications", "users"
add_foreign_key "place_visits", "places"
add_foreign_key "place_visits", "visits"