Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion app/controllers/application_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ def current_user
end

def require_current_user!
redirect_to new_session_path if current_user.blank?
if current_user.blank?
redirect_to new_session_path, allow_other_host: false, status: :see_other
end
end
end
19 changes: 19 additions & 0 deletions app/javascript/controllers/offline_controller.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
connect() {
this.updateStatus()

window.addEventListener('online', this.updateStatus.bind(this))
window.addEventListener('offline', this.updateStatus.bind(this))
}

disconnect() {
window.removeEventListener('online', this.updateStatus.bind(this))
window.removeEventListener('offline', this.updateStatus.bind(this))
}

updateStatus() {
this.element.classList.toggle('invisible', navigator.onLine)
}
}
5 changes: 4 additions & 1 deletion app/models/user.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,19 @@ class User < ApplicationRecord
RATE_LIMIT_WINDOW = 15.minutes
MAX_EMAIL_ATTEMPTS = 3

# TODO: soft delete for users?
# Associations
has_many :credentials, dependent: :destroy
has_many :challenge_story_likes, dependent: :destroy
has_many :liked_challenge_stories, through: :challenge_story_likes, source: :challenge_story
# soft delete challenges?
has_many :challenge_participants, dependent: :destroy
# soft delete stories?
has_many :challenge_stories, through: :challenge_participants

# Validations
validates :username, presence: true, uniqueness: true
validates :email, presence: false, uniqueness: { allow_nil: true }, format: { with: URI::MailTo::EMAIL_REGEXP, allow_nil: true }
validates :email, presence: false, uniqueness: {allow_nil: true}, format: {with: URI::MailTo::EMAIL_REGEXP, allow_nil: true}

# Callbacks
after_initialize do
Expand Down
23 changes: 17 additions & 6 deletions app/views/layouts/_navbar.html.erb
Original file line number Diff line number Diff line change
@@ -1,12 +1,23 @@
<div class="navbar bg-base-100 shadow-sm">
<div class="flex-1">
<%= link_to "Strivo", root_path, class: "btn btn-ghost text-xl" %>
<div class="navbar-start">
<div class="flex-1">
<%= link_to "Strivo", root_path, class: "btn btn-ghost text-xl" %>
</div>
</div>

<%#
<div class="flex gap-2">
<input type="text" placeholder="Search" class="input input-bordered w-24 md:w-auto" />
<div class="navbar-center invisible" data-controller="offline">
<div class="badge badge-error">
<svg class="size-[1em]" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g fill="currentColor"><rect x="1.972" y="11" width="20.056" height="2" transform="translate(-4.971 12) rotate(-45)" fill="currentColor" stroke-width="0"></rect><path d="m12,23c-6.065,0-11-4.935-11-11S5.935,1,12,1s11,4.935,11,11-4.935,11-11,11Zm0-20C7.038,3,3,7.037,3,12s4.038,9,9,9,9-4.037,9-9S16.962,3,12,3Z" stroke-width="0" fill="currentColor"></path></g></svg>
You are offline
</div>
</div>

<div class="navbar-end">
<%#
<div class="flex gap-2">
<input type="text" placeholder="Search" class="input input-bordered w-24 md:w-auto" />
</div>
%>
</div>
%>

</div>
83 changes: 82 additions & 1 deletion app/views/pwa/service-worker.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,85 @@
console.log('Service worker loaded.');
const VERSION = 'v20260104'; // Version will be the key, increment to invalidate old caches
console.log('Service worker loaded. Version:', VERSION);

async function networkFirst(request) {
const cache = await caches.open(VERSION);

try {
const responseFromNetwork = await fetch(request.clone());

if (responseFromNetwork.ok) {
cache.put(request, responseFromNetwork.clone());
}

return responseFromNetwork;
} catch (error) {
const cachedResponse = await cache.match(request);

if (cachedResponse) {
return cachedResponse;
}

return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
});
}
}

async function cacheFirst(request) {
const cache = await caches.open(VERSION);
const cachedResponse = await caches.match(request);

if (cachedResponse) {
return cachedResponse;
}

try {
const responseFromNetwork = await fetch(request.clone());

cache.put(request, responseFromNetwork.clone());

return responseFromNetwork;
} catch (error) {
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
});
}
}

function isNavigationRequest(request) {
return request.mode === 'navigate';
}

function isAPIRequest(request) {
return request.url.includes('/api/') || request.url.includes('.json');
}

self.addEventListener('fetch', function(event) {
// Use network-first for HTML pages (navigation requests) so Turbo streams work
if (isNavigationRequest(event.request)) {
event.respondWith(networkFirst(event.request));
}
// Use cache-first for static assets and other resources
else {
event.respondWith(cacheFirst(event.request));
}
});

self.addEventListener('activate', function(event) {
event.waitUntil(
caches.keys().then(function(cacheNames) {
return Promise.all(
cacheNames.map(function(cacheName) {
if (cacheName !== VERSION) {
return caches.delete(cacheName);
}
})
);
})
);
});
// Add a service worker for processing Web Push notifications:
//
// self.addEventListener("push", async (event) => {
Expand Down