Skip to content
Merged
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
70 changes: 70 additions & 0 deletions .github/workflows/feature-docker.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
name: Feature Branch Docker

on:
push:
branches-ignore:
- main

permissions:
contents: read
packages: write

jobs:
build-and-push:
name: Build & push Docker images
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Sanitize branch name for Docker tag
id: branch
run: |
RAW="${GITHUB_REF_NAME}"
SANITIZED=$(echo "$RAW" | sed 's/[^a-zA-Z0-9._-]/-/g' | sed 's/^-//;s/-$//' | cut -c1-128)
echo "TAG=$SANITIZED" >> "$GITHUB_OUTPUT"

- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build and push frontend image
uses: docker/build-push-action@v5
with:
context: ./frontend
file: ./frontend/Dockerfile.prod
push: true
tags: ghcr.io/${{ github.repository }}/frontend:${{ steps.branch.outputs.TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Build and push backend image
uses: docker/build-push-action@v5
with:
context: ./backend
file: ./backend/Dockerfile.prod
push: true
tags: ghcr.io/${{ github.repository }}/backend:${{ steps.branch.outputs.TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max

- name: Summary
run: |
echo "### 🐳 Docker images pushed" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Service | Image |" >> "$GITHUB_STEP_SUMMARY"
echo "|---------|-------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Frontend | \`ghcr.io/${{ github.repository }}/frontend:${{ steps.branch.outputs.TAG }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "| Backend | \`ghcr.io/${{ github.repository }}/backend:${{ steps.branch.outputs.TAG }}\` |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Pull with:" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`bash" >> "$GITHUB_STEP_SUMMARY"
echo "docker pull ghcr.io/${{ github.repository }}/frontend:${{ steps.branch.outputs.TAG }}" >> "$GITHUB_STEP_SUMMARY"
echo "docker pull ghcr.io/${{ github.repository }}/backend:${{ steps.branch.outputs.TAG }}" >> "$GITHUB_STEP_SUMMARY"
echo "\`\`\`" >> "$GITHUB_STEP_SUMMARY"
17 changes: 11 additions & 6 deletions backend/src/main/kotlin/com/shoppinglist/routes/ListRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ fun Route.listRoutes(pushNotificationService: PushNotificationService? = null) {
call.application.log.warn("Error broadcasting item addition: ${e.message}")
}

val deviceId = call.request.header("X-Device-Id")
val deviceId = call.request.queryParameters["deviceId"]
call.application.launch(Dispatchers.Default) {
pushNotificationService?.notifyListChange(listId, deviceId, "ITEM_ADDED", newItem.text)
}
Expand Down Expand Up @@ -273,9 +273,13 @@ fun Route.listRoutes(pushNotificationService: PushNotificationService? = null) {
call.application.log.warn("Error broadcasting item update: ${e.message}")
}

val deviceId = call.request.header("X-Device-Id")
val deviceId = call.request.queryParameters["deviceId"]
val pushChangeType = when {
request.completed != null -> if (updatedItem.completed) "ITEM_CHECKED" else "ITEM_UNCHECKED"
else -> "ITEM_UPDATED"
}
call.application.launch(Dispatchers.Default) {
pushNotificationService?.notifyListChange(listId, deviceId, "ITEM_UPDATED", updatedItem.text)
pushNotificationService?.notifyListChange(listId, deviceId, pushChangeType, updatedItem.text)
}

call.respond(HttpStatusCode.OK, updatedItem)
Expand Down Expand Up @@ -330,6 +334,7 @@ fun Route.listRoutes(pushNotificationService: PushNotificationService? = null) {
return@delete
}

val itemToDelete = itemRepository.getItemById(itemId)
val deleted = itemRepository.deleteItem(itemId)
if (!deleted) {
call.respond(
Expand All @@ -351,9 +356,9 @@ fun Route.listRoutes(pushNotificationService: PushNotificationService? = null) {
call.application.log.warn("Error broadcasting item deletion: ${e.message}")
}

val deviceId = call.request.header("X-Device-Id")
val deviceId = call.request.queryParameters["deviceId"]
call.application.launch(Dispatchers.Default) {
pushNotificationService?.notifyListChange(listId, deviceId, "ITEM_DELETED", null)
pushNotificationService?.notifyListChange(listId, deviceId, "ITEM_DELETED", itemToDelete?.text)
}

call.respond(HttpStatusCode.NoContent)
Expand Down Expand Up @@ -409,7 +414,7 @@ fun Route.listRoutes(pushNotificationService: PushNotificationService? = null) {
call.application.log.warn("Error broadcasting items cleared: ${e.message}")
}

val deviceId = call.request.header("X-Device-Id")
val deviceId = call.request.queryParameters["deviceId"]
call.application.launch(Dispatchers.Default) {
pushNotificationService?.notifyListChange(listId, deviceId, "ITEMS_CLEARED", null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,16 +54,26 @@ class PushNotificationService(
}

private fun buildPayload(changeType: String, listId: UUID, itemText: String?): String {
val bodyText = when {
itemText != null -> when (changeType) {
"ITEM_ADDED" -> "$itemText added"
"ITEM_UPDATED" -> "$itemText updated"
"ITEM_DELETED" -> "$itemText removed"
else -> "List updated"
}
val title = when (changeType) {
"ITEM_ADDED" -> "Item added"
"ITEM_CHECKED" -> "Item checked off"
"ITEM_UNCHECKED" -> "Item unchecked"
"ITEM_UPDATED" -> "Item updated"
"ITEM_DELETED" -> "Item removed"
"ITEMS_CLEARED" -> "Completed items cleared"
else -> "List updated"
}.take(40)
return """{"type":"$changeType","listId":"$listId","title":"List updated","body":"$bodyText","url":"/list/$listId"}"""
}
val bodyText = when (changeType) {
"ITEM_ADDED" -> itemText?.let { "✚ $it" } ?: "New item added"
"ITEM_CHECKED" -> itemText?.let { "✓ $it" } ?: "An item was checked off"
"ITEM_UNCHECKED" -> itemText?.let { "↩ $it" } ?: "An item was unchecked"
"ITEM_UPDATED" -> itemText?.let { "✎ $it" } ?: "An item was edited"
"ITEM_DELETED" -> itemText?.let { "✕ $it" } ?: "An item was removed"
"ITEMS_CLEARED" -> "All completed items were removed"
else -> "The list was updated"
}.take(60)
val escaped = bodyText.replace("\\", "\\\\").replace("\"", "\\\"")
return """{"type":"$changeType","listId":"$listId","title":"$title","body":"$escaped","url":"/list/$listId"}"""
}

companion object {
Expand Down
17 changes: 14 additions & 3 deletions frontend/src/components/NotificationBell.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useEffect, useRef } from 'react';
import { useNotifications } from '../context/PushNotificationContext';
import { useI18n } from '../context/I18nContext';

Expand All @@ -6,19 +7,29 @@ interface NotificationBellProps {
}

const NotificationBell = ({ listId }: NotificationBellProps) => {
const { permissionStatus, isSubscribed, isLoading, requestPermission, subscribe, unsubscribe } = useNotifications();
const { permissionStatus, isSubscribed, isLoading, subscribe, unsubscribe, autoSubscribe, setOptedOut } = useNotifications();
const { t } = useI18n();
const autoSubscribeAttempted = useRef(false);

useEffect(() => {
if (autoSubscribeAttempted.current) return;
autoSubscribeAttempted.current = true;
autoSubscribe(listId);
}, [listId, autoSubscribe]);

const handleClick = async () => {
if (permissionStatus === 'denied') {
return;
}

if (isSubscribed) {
setOptedOut(listId, true);
await unsubscribe(listId);
} else {
setOptedOut(listId, false);
if (permissionStatus !== 'granted') {
await requestPermission();
const result = await Notification.requestPermission();
if (result !== 'granted') return;
}
await subscribe(listId);
}
Expand Down Expand Up @@ -82,7 +93,7 @@ const NotificationBell = ({ listId }: NotificationBellProps) => {
? 'opacity-50 cursor-not-allowed bg-gray-200 dark:bg-gray-700 text-gray-500 dark:text-gray-400'
: isSubscribed
? 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300 border border-green-200 dark:border-green-800 hover:bg-green-200 dark:hover:bg-green-800/50'
: 'bg-blue-600 dark:bg-blue-500 hover:bg-blue-700 dark:hover:bg-blue-600 text-white'
: 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-600 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{getBellIcon()}
Expand Down
Loading
Loading