New `mobile` Django app mounted at /app/ -- the installed PWA for Member mode (M1-M7, see design_handoff_rosterchief_platform/README.md). Coach mode (C1-C6) is a later phase and has no routes yet. Foundation pieces: - assets/mobile.css: Tailwind v4 theme reusing management.css's design tokens (same per-club --tenant-* theming pattern), plus the ice/coach accent and mobile's 14px card radius. - PushSubscription model + pywebpush-based sender (mobile/services/push.py), wired to notifications.Notification via a post_save signal so the existing notification system gains a push channel without knowing about PWAs itself. - Per-club manifest.webmanifest + service worker (served at /app/sw.js) + a server-rendered fallback home-screen icon (club initials on secondary_color) for clubs without an uploaded logo -- confirmed with the user as the fallback, never a generic RosterChief mark. - App shell (base.html): navy header, person switcher (every child a signed-in parent manages, plus "Me"), bottom tab bar, safe-area insets. Vendored htmx + Alpine for the screens built on top of it. - Placeholder views/routes for all seven M1-M7 screens so the shell is fully wired end-to-end before each screen is built out individually. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ECGMEwrc2k4D8VQuwjstj9
37 lines
1.4 KiB
Python
37 lines
1.4 KiB
Python
"""Server-rendered fallback PWA icon: a club's initials on its own
|
|
secondary_color -- confirmed with the user as the fallback for a club that
|
|
hasn't uploaded a logo, rather than a shared RosterChief mark (Club.initials'
|
|
own docstring: "Never the RosterChief mark -- that would pass our branding
|
|
off as the club's own", same reasoning applied to the home-screen icon).
|
|
|
|
Rendered on request by mobile.views.AppIconView, not stored -- a club's
|
|
colours/initials change rarely enough that regenerating a small PNG per
|
|
request is cheaper than adding cache invalidation for it.
|
|
"""
|
|
|
|
import io
|
|
|
|
from PIL import Image, ImageDraw, ImageFont
|
|
|
|
_DEFAULT_BACKGROUND = "#e4002b"
|
|
_DEFAULT_FOREGROUND = "#ffffff"
|
|
|
|
|
|
def render_fallback_icon(club, size: int = 512) -> bytes:
|
|
background = club.secondary_color or _DEFAULT_BACKGROUND
|
|
foreground = club.secondary_content_color or _DEFAULT_FOREGROUND
|
|
|
|
image = Image.new("RGB", (size, size), background)
|
|
draw = ImageDraw.Draw(image)
|
|
|
|
initials = club.initials or "RC"
|
|
font = ImageFont.load_default(size=int(size * 0.42))
|
|
|
|
left, top, right, bottom = draw.textbbox((0, 0), initials, font=font)
|
|
text_width, text_height = right - left, bottom - top
|
|
draw.text(((size - text_width) / 2 - left, (size - text_height) / 2 - top), initials, font=font, fill=foreground)
|
|
|
|
buffer = io.BytesIO()
|
|
image.save(buffer, format="PNG")
|
|
return buffer.getvalue()
|