Skip to content

Commit 721ce97

Browse files
committed
Rename Scan model to Visit with QR/link source distinction
- Rename scans table to visits with source column (qr_code/link) - Add source detection via ?qr=1 parameter for QR codes - Differentiated ticket rewards: QR scans = 3 tickets, links = 1 ticket - Update all controllers, views, routes, and jobs - Add visual indicators (📱 for QR, 🔗 for links) throughout UI - Configure production database environment variables - Add offline poster upload functionality with service worker caching
1 parent ac294b3 commit 721ce97

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+1860
-149
lines changed

PRODUCTION_ENV.md

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# Production Environment Variables
2+
3+
## Required Database Configuration
4+
5+
```bash
6+
# Database Connection
7+
DATABASE_HOST=your-postgres-host.com
8+
DATABASE_PORT=5432
9+
DATABASE_NAME=pyramid_scheme_production # Base name, suffixes added automatically
10+
DATABASE_USERNAME=your_db_user
11+
DATABASE_PASSWORD=your_secure_password
12+
```
13+
14+
## Rails Configuration
15+
16+
```bash
17+
# Rails Environment
18+
RAILS_ENV=production
19+
RAILS_MASTER_KEY=your_master_key_from_credentials
20+
SECRET_KEY_BASE=your_long_random_secret
21+
22+
# Connection Pool
23+
RAILS_MAX_THREADS=5
24+
```
25+
26+
## Application Specific
27+
28+
```bash
29+
# Geocoding Service
30+
GEOCODER_API_KEY=your_geocoder_api_key
31+
32+
# Base URL for referral links
33+
BASE_REFERRAL_URL=https://your-domain.com
34+
35+
# OAuth (if using GitHub auth)
36+
GITHUB_CLIENT_ID=your_github_client_id
37+
GITHUB_CLIENT_SECRET=your_github_client_secret
38+
```
39+
40+
## Database Setup
41+
42+
Rails will automatically create these databases during deployment:
43+
- `pyramid_scheme_production` (main app data)
44+
- `pyramid_scheme_production_cache` (Rails cache)
45+
- `pyramid_scheme_production_queue` (background jobs)
46+
- `pyramid_scheme_production_cable` (websockets)
47+
48+
## Docker Deployment
49+
50+
With Docker, Rails handles database setup automatically when you run:
51+
```bash
52+
bin/rails db:create db:migrate RAILS_ENV=production
53+
```

app/controllers/admin_controller.rb

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,17 @@ def index
1010
.where("birthday IS NOT NULL AND EXTRACT(year FROM age(birthday)) <= 18").count
1111
}
1212

13-
@scan_stats = {
14-
total_scans: Scan.count,
15-
unique_visitors: Scan.select(:ip_address, :user_agent).distinct.count,
16-
scans_today: Scan.where('created_at > ?', 1.day.ago).count,
17-
scans_this_week: Scan.where('created_at > ?', 1.week.ago).count
13+
@visit_stats = {
14+
total_visits: Visit.count,
15+
unique_visitors: Visit.select(:ip_address, :user_agent).distinct.count,
16+
visits_today: Visit.where('created_at > ?', 1.day.ago).count,
17+
visits_this_week: Visit.where('created_at > ?', 1.week.ago).count,
18+
qr_visits: Visit.qr_code.count,
19+
link_visits: Visit.link.count
1820
}
1921

2022
@recent_users = User.order(created_at: :desc).limit(10)
21-
@recent_scans = Scan.includes(:user).order(created_at: :desc).limit(10)
23+
@recent_visits = Visit.includes(:user).order(created_at: :desc).limit(10)
2224
end
2325

2426
private

app/controllers/dashboard_controller.rb

Lines changed: 38 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,25 @@ class DashboardController < ApplicationController
44

55
def show
66
@user = current_user
7-
@scans = current_user.scans.includes(:user).recent.limit(20)
8-
@scan_stats = calculate_scan_stats
9-
@next_prize = calculate_next_prize(@scan_stats[:total_tickets])
7+
@visits = current_user.visits.includes(:user).recent.limit(20)
8+
@visit_stats = calculate_visit_stats
9+
@next_prize = calculate_next_prize(@visit_stats[:total_tickets])
1010
end
1111

12-
def scan_data
13-
scans = current_user.scans.geocoded.recent.limit(100)
12+
def visit_data
13+
visits = current_user.visits.geocoded.recent.limit(100)
1414

15-
data = scans.map do |scan|
15+
data = visits.map do |visit|
1616
{
17-
id: scan.id,
17+
id: visit.id,
1818
username: current_user.username,
19-
latitude: scan.latitude,
20-
longitude: scan.longitude,
21-
location: scan.location_display,
22-
device: scan.device_info,
23-
created_at: scan.created_at.iso8601,
24-
time_ago: time_ago_in_words(scan.created_at)
19+
latitude: visit.latitude,
20+
longitude: visit.longitude,
21+
location: visit.location_display,
22+
device: visit.device_info,
23+
source: visit.source,
24+
created_at: visit.created_at.iso8601,
25+
time_ago: time_ago_in_words(visit.created_at)
2526
}
2627
end
2728

@@ -32,8 +33,9 @@ def poster
3233
require 'hexapdf'
3334
require 'rqrcode'
3435

35-
# Generate QR code
36-
qr_code = RQRCode::QRCode.new(current_user.referral_url)
36+
# Generate QR code with qr=1 parameter for source detection
37+
qr_url = "#{current_user.referral_url}?qr=1"
38+
qr_code = RQRCode::QRCode.new(qr_url)
3739
qr_png = qr_code.as_png(size: 400, border_modules: 2)
3840

3941
# Create PDF document
@@ -144,30 +146,34 @@ def poster
144146

145147
private
146148

147-
def calculate_scan_stats
148-
scans = current_user.scans
149-
scans_this_week = scans.where('created_at > ?', 1.week.ago)
149+
def calculate_visit_stats
150+
visits = current_user.visits
151+
visits_this_week = visits.where('created_at > ?', 1.week.ago)
150152

151-
# Calculate tickets based on current activity
152-
# For now, we'll simulate ticket earning:
153-
# - 3 tickets per unique visitor (simulating signup + 1 hour coding)
154-
# - 1 ticket per poster download (we'll estimate based on total scans / 10)
153+
# Calculate tickets based on source and activity
154+
# QR code scans: 3 tickets each (higher engagement)
155+
# Link clicks: 1 ticket each (easier sharing)
155156

156-
total_tickets = (scans.select(:ip_address, :user_agent).distinct.count * 3) +
157-
(scans.count / 10) # Estimate poster downloads
157+
qr_visits = visits.qr_code.select(:ip_address, :user_agent).distinct
158+
link_visits = visits.link.select(:ip_address, :user_agent).distinct
158159

159-
tickets_this_week = (scans_this_week.select(:ip_address, :user_agent).distinct.count * 3) +
160-
(scans_this_week.count / 10)
160+
qr_visits_week = visits_this_week.qr_code.select(:ip_address, :user_agent).distinct
161+
link_visits_week = visits_this_week.link.select(:ip_address, :user_agent).distinct
162+
163+
total_tickets = (qr_visits.count * 3) + (link_visits.count * 1)
164+
tickets_this_week = (qr_visits_week.count * 3) + (link_visits_week.count * 1)
161165

162166
{
163-
total_scans: scans.count,
167+
total_visits: visits.count,
164168
total_tickets: total_tickets,
165-
unique_visitors: scans.select(:ip_address, :user_agent).distinct.count,
166-
scans_today: scans.where('created_at > ?', 1.day.ago).count,
167-
scans_this_week: scans_this_week.count,
169+
unique_visitors: visits.select(:ip_address, :user_agent).distinct.count,
170+
qr_visits: qr_visits.count,
171+
link_visits: link_visits.count,
172+
visits_today: visits.where('created_at > ?', 1.day.ago).count,
173+
visits_this_week: visits_this_week.count,
168174
tickets_this_week: tickets_this_week,
169-
unique_visitors_this_week: scans_this_week.select(:ip_address, :user_agent).distinct.count,
170-
top_countries: scans.geocoded
175+
unique_visitors_this_week: visits_this_week.select(:ip_address, :user_agent).distinct.count,
176+
top_countries: visits.geocoded
171177
.where.not(country_name: [nil, ''])
172178
.group(:country_name)
173179
.count

app/controllers/home_controller.rb

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,28 @@
11
class HomeController < ApplicationController
22
include ActionView::Helpers::DateHelper
33
def index
4-
@recent_scans = Scan.includes(:user)
5-
.recent
6-
.limit(10)
7-
@unique_scan_count = Rails.cache.fetch("unique_scan_count", expires_in: 15.seconds) do
8-
Scan.select(:ip_address, :user_agent).distinct.count
4+
@recent_visits = Visit.includes(:user)
5+
.recent
6+
.limit(10)
7+
@unique_visit_count = Rails.cache.fetch("unique_visit_count", expires_in: 15.seconds) do
8+
Visit.select(:ip_address, :user_agent).distinct.count
99
end
1010
end
1111

12-
def scan_data
13-
scans = Scan.includes(:user).geocoded.recent.limit(100)
12+
def visit_data
13+
visits = Visit.includes(:user).geocoded.recent.limit(100)
1414

15-
data = scans.map do |scan|
15+
data = visits.map do |visit|
1616
{
17-
id: scan.id,
18-
username: scan.user&.username || "Unknown",
19-
latitude: scan.latitude,
20-
longitude: scan.longitude,
21-
location: scan.location_display,
22-
device: scan.device_info,
23-
created_at: scan.created_at.iso8601,
24-
time_ago: time_ago_in_words(scan.created_at)
17+
id: visit.id,
18+
username: visit.user&.username || "Unknown",
19+
latitude: visit.latitude,
20+
longitude: visit.longitude,
21+
location: visit.location_display,
22+
device: visit.device_info,
23+
source: visit.source,
24+
created_at: visit.created_at.iso8601,
25+
time_ago: time_ago_in_words(visit.created_at)
2526
}
2627
end
2728

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
class PostersController < ApplicationController
2+
before_action :require_login
3+
before_action :set_poster, only: [:show]
4+
5+
def index
6+
@posters = current_user.posters.order(created_at: :desc)
7+
end
8+
9+
def new
10+
@poster = current_user.posters.build
11+
end
12+
13+
def create
14+
@poster = current_user.posters.build(poster_params)
15+
16+
if @poster.save
17+
# Queue background job for processing
18+
ProcessPosterJob.perform_later(@poster)
19+
20+
redirect_to @poster, notice: 'Poster uploaded successfully! It will be reviewed shortly.'
21+
else
22+
render :new, status: :unprocessable_entity
23+
end
24+
end
25+
26+
def show
27+
# Show poster details and status
28+
end
29+
30+
private
31+
32+
def set_poster
33+
@poster = current_user.posters.find(params[:id])
34+
end
35+
36+
def poster_params
37+
params.require(:poster).permit(:image, :latitude, :longitude)
38+
end
39+
end

app/controllers/referrals_controller.rb

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,18 @@ def show
2020
private
2121

2222
def log_scan(referral_code)
23-
# Enqueue job to create scan with geocoding - don't block the redirect
24-
CreateScanJob.perform_later(
23+
# Detect source: QR code vs link click
24+
source = params[:qr] == '1' ? 'qr_code' : 'link'
25+
26+
# Enqueue job to create visit with geocoding - don't block the redirect
27+
CreateVisitJob.perform_later(
2528
referral_code,
2629
request.remote_ip,
27-
request.user_agent
30+
request.user_agent,
31+
source
2832
)
2933
rescue => e
3034
# Log error but don't fail the redirect
31-
Rails.logger.error "Failed to enqueue scan creation: #{e.message}"
35+
Rails.logger.error "Failed to enqueue visit creation: #{e.message}"
3236
end
3337
end

app/helpers/posters_helper.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
module PostersHelper
2+
end

app/javascript/application.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,21 @@
11
// Configure your import map in config/importmap.rb. Read more: https://github.com/rails/importmap-rails
22
import "@hotwired/turbo-rails"
33
import "controllers"
4+
5+
// Register service worker for offline functionality
6+
if ('serviceWorker' in navigator) {
7+
window.addEventListener('load', () => {
8+
navigator.serviceWorker.register('/sw.js')
9+
.then(registration => {
10+
console.log('SW registered: ', registration);
11+
12+
// Request persistent storage for offline uploads
13+
if ('storage' in navigator && 'persist' in navigator.storage) {
14+
navigator.storage.persist();
15+
}
16+
})
17+
.catch(registrationError => {
18+
console.log('SW registration failed: ', registrationError);
19+
});
20+
});
21+
}

0 commit comments

Comments
 (0)