mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
Use ids instead of uuids for primary keys in family tables
This commit is contained in:
parent
698198db4b
commit
db8d886ee2
20 changed files with 738 additions and 1802 deletions
2012
FAMILY_PLAN.md
2012
FAMILY_PLAN.md
File diff suppressed because it is too large
Load diff
File diff suppressed because one or more lines are too long
1
app/assets/svg/icons/lucide/outline/chart-column.svg
Normal file
1
app/assets/svg/icons/lucide/outline/chart-column.svg
Normal 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 |
1
app/assets/svg/icons/lucide/outline/heart.svg
Normal file
1
app/assets/svg/icons/lucide/outline/heart.svg
Normal 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 |
1
app/assets/svg/icons/lucide/outline/shield-check.svg
Normal file
1
app/assets/svg/icons/lucide/outline/shield-check.svg
Normal 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 |
1
app/assets/svg/icons/lucide/outline/users.svg
Normal file
1
app/assets/svg/icons/lucide/outline/users.svg
Normal 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 |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
35
app/controllers/users/sessions_controller.rb
Normal file
35
app/controllers/users/sessions_controller.rb
Normal 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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
56
db/migrate/20250930150256_convert_family_tables_to_bigint.rb
Normal file
56
db/migrate/20250930150256_convert_family_tables_to_bigint.rb
Normal 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
25
db/schema.rb
generated
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Reference in a new issue