feat: timezone auto-detection, branding fix, frontend rebuild
Timezone: - Add diligence/utils/dates.py with today_for_user(), now_for_user(), day_start_utc() - Replace all 20 date.today() calls across 10 files with timezone-aware helpers - Remove hardcoded Asia/Bangkok offset hack in nutrition router - Change defaults from Asia/Bangkok to UTC in user model, nutrition model, config - Add timezone field to RegisterRequest schema - Set user.timezone from browser detection during registration - Frontend sends Intl.DateTimeFormat().resolvedOptions().timeZone on register Branding: - Login page: Fitness Rewards -> Diligence, updated tagline and emoji - index.html title: Fitness Rewards -> Diligence Frontend rebuild: - Fresh Vite build with all changes (index-CsKoBN0D.js) - Nav bar, timezone detection, and branding all verified in built bundle
This commit is contained in:
parent
40c8485aa7
commit
c37543b009
21 changed files with 1961 additions and 64 deletions
|
|
@ -40,7 +40,7 @@ class Settings(BaseSettings):
|
|||
|
||||
# App
|
||||
base_url: str = "http://localhost:8000"
|
||||
timezone: str = "Asia/Bangkok"
|
||||
timezone: str = "UTC"
|
||||
data_dir: str = str(_default_data_dir())
|
||||
|
||||
# MCP
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
|
@ -6,8 +6,8 @@
|
|||
<meta name="theme-color" content="#1a1a2e" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<title>Fitness Rewards</title>
|
||||
<script type="module" crossorigin src="/assets/index-De1TwOuS.js"></script>
|
||||
<title>Diligence</title>
|
||||
<script type="module" crossorigin src="/assets/index-CsKoBN0D.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-ciTnvXdZ.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ class NutritionGoal(Base):
|
|||
default_fast_hours: Mapped[int] = mapped_column(Integer, default=16) # 16:8
|
||||
|
||||
diet_style: Mapped[str] = mapped_column(String(30), default="strict_keto")
|
||||
timezone_str: Mapped[str] = mapped_column(String(50), default="Asia/Bangkok")
|
||||
timezone_str: Mapped[str] = mapped_column(String(50), default="UTC")
|
||||
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@ class User(Base):
|
|||
email: Mapped[str | None] = mapped_column(String(255), nullable=True)
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc))
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, default=True)
|
||||
timezone: Mapped[str] = mapped_column(String(50), default="Asia/Bangkok")
|
||||
timezone: Mapped[str] = mapped_column(String(50), default="UTC")
|
||||
is_admin: Mapped[bool] = mapped_column(Boolean, default=False)
|
||||
|
||||
# Relationships
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ async def register(req: RegisterRequest, db: Annotated[AsyncSession, Depends(get
|
|||
display_name=req.display_name,
|
||||
password_hash=hash_password(req.password),
|
||||
email=req.email,
|
||||
timezone=req.timezone or "UTC",
|
||||
is_admin=is_first_user,
|
||||
)
|
||||
db.add(user)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from diligence.models.catalog import ProgramCatalog, CatalogWorkout, CrawlQueue,
|
|||
from diligence.services.program_research import slugify, find_urls_for_program
|
||||
from diligence.services.points_engine import log_activity_with_points
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import today_for_user
|
||||
|
||||
router = APIRouter(prefix="/api/programs", tags=["programs-v2"])
|
||||
|
||||
|
|
@ -337,7 +338,7 @@ async def get_program_schedule(
|
|||
)
|
||||
|
||||
# Calculate current real week from start date
|
||||
today = date.today()
|
||||
today = today_for_user(user.timezone)
|
||||
days_elapsed = max(0, (today - program.start_date).days)
|
||||
current_week = min(total_weeks, (days_elapsed // 7) + 1)
|
||||
|
||||
|
|
@ -491,7 +492,7 @@ async def complete_workout(
|
|||
db=db,
|
||||
user_id=user.id,
|
||||
category="workout",
|
||||
activity_date=date.today(),
|
||||
activity_date=today_for_user(user.timezone),
|
||||
title=f"{program.name}: Wk{real_week} {workout.workout_name or f'Day {workout.day_number}'}",
|
||||
description=f"Completed program workout ({len(workout.exercises)} exercises)",
|
||||
source="program",
|
||||
|
|
@ -556,7 +557,7 @@ async def check_weekly_bonus(
|
|||
db=db,
|
||||
user_id=user.id,
|
||||
category="bonus",
|
||||
activity_date=date.today(),
|
||||
activity_date=today_for_user(user.timezone),
|
||||
title=f"{program.name}: Week {real_week} Complete!",
|
||||
source="program",
|
||||
program_id=program.id,
|
||||
|
|
@ -599,7 +600,7 @@ async def check_completion_bonus(
|
|||
db=db,
|
||||
user_id=user.id,
|
||||
category="bonus",
|
||||
activity_date=date.today(),
|
||||
activity_date=today_for_user(user.timezone),
|
||||
title=f"{program.name}: Program Complete!",
|
||||
source="program",
|
||||
program_id=program.id,
|
||||
|
|
@ -654,7 +655,7 @@ async def get_program_progress(
|
|||
)
|
||||
total_points = result.scalar() or 0
|
||||
|
||||
today = date.today()
|
||||
today = today_for_user(user.timezone)
|
||||
days_elapsed = max(0, (today - program.start_date).days)
|
||||
current_week = min(total_weeks, (days_elapsed // 7) + 1)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from diligence.models.user import User
|
|||
from diligence.models.food import FoodLog
|
||||
from diligence.schemas.food import FoodCreate, FoodSearchResult
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import today_for_user
|
||||
from diligence.services.food_lookup import lookup_barcode, search_food
|
||||
|
||||
router = APIRouter(prefix="/api/food", tags=["food"])
|
||||
|
|
@ -49,7 +50,7 @@ async def list_food(
|
|||
if item.calories:
|
||||
total_cals += float(item.calories) * float(item.servings or 1)
|
||||
|
||||
return {"date": (food_date or date.today()).isoformat(), "meals": meals, "total_calories": round(total_cals, 1)}
|
||||
return {"date": (food_date or today_for_user(user.timezone)).isoformat(), "meals": meals, "total_calories": round(total_cals, 1)}
|
||||
|
||||
|
||||
@router.post("")
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from diligence.database import get_db
|
|||
from diligence.models.user import User
|
||||
from diligence.models.meal_plan import MealPlan, MealPlanItem, MealCompliance
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import today_for_user
|
||||
|
||||
router = APIRouter(prefix="/api/meal-plans", tags=["meal-plans"])
|
||||
|
||||
|
|
@ -94,7 +95,7 @@ async def create_meal_plan(
|
|||
daily_fat_g=body.daily_fat_g,
|
||||
restrictions=body.restrictions,
|
||||
duration_days=body.duration_days,
|
||||
start_date=body.start_date or date.today(),
|
||||
start_date=body.start_date or today_for_user(user.timezone),
|
||||
)
|
||||
db.add(plan)
|
||||
await db.flush()
|
||||
|
|
@ -131,7 +132,7 @@ async def get_today_meals(
|
|||
if not plan:
|
||||
return {"active_plan": None}
|
||||
|
||||
day_num = (date.today() - plan.start_date).days + 1
|
||||
day_num = (today_for_user(user.timezone) - plan.start_date).days + 1
|
||||
if day_num < 1 or day_num > plan.duration_days:
|
||||
return {"active_plan": plan.name, "day": day_num, "meals": [], "message": "No meals planned for today"}
|
||||
|
||||
|
|
@ -217,7 +218,7 @@ async def log_compliance(
|
|||
user_id=user.id,
|
||||
plan_id=plan.id,
|
||||
plan_item_id=uuid_mod.UUID(body.plan_item_id) if body.plan_item_id else None,
|
||||
compliance_date=body.compliance_date or date.today(),
|
||||
compliance_date=body.compliance_date or today_for_user(user.timezone),
|
||||
status=body.status,
|
||||
substitution=body.substitution,
|
||||
)
|
||||
|
|
@ -250,7 +251,7 @@ async def get_plan_progress(
|
|||
substituted = sum(1 for e in entries if e.status == "substituted")
|
||||
skipped = sum(1 for e in entries if e.status == "skipped")
|
||||
|
||||
days_elapsed = (date.today() - plan.start_date).days + 1
|
||||
days_elapsed = (today_for_user(user.timezone) - plan.start_date).days + 1
|
||||
compliance_pct = (followed + substituted) / total * 100 if total > 0 else 0
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from diligence.schemas.nutrition import (
|
|||
)
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.services.points_engine import log_activity_with_points
|
||||
from diligence.utils.dates import today_for_user, now_for_user, day_start_utc
|
||||
|
||||
router = APIRouter(prefix="/api/nutrition", tags=["nutrition"])
|
||||
|
||||
|
|
@ -87,7 +88,7 @@ async def get_today(
|
|||
):
|
||||
"""Today's macros vs target, eating-window state, active fast, compliance."""
|
||||
goal = await _get_or_create_goal(db, user.id)
|
||||
today = date.today()
|
||||
today = today_for_user(user.timezone)
|
||||
|
||||
# Sum today's food
|
||||
result = await db.execute(
|
||||
|
|
@ -103,8 +104,8 @@ async def get_today(
|
|||
cals, prot, carbs, fat, fiber = float(cals), float(prot), float(carbs), float(fat), float(fiber)
|
||||
net_carbs = max(0.0, carbs - fiber)
|
||||
|
||||
# Eating window (local time, using goal timezone_str — simplified: assume server in UTC, user sends local)
|
||||
now_local = datetime.now(timezone.utc) + timedelta(hours=7) # Asia/Bangkok offset hack
|
||||
# Eating window (user's local time)
|
||||
now_local = now_for_user(user.timezone)
|
||||
hour = now_local.hour
|
||||
in_window = goal.eating_window_start_hour <= hour < goal.eating_window_end_hour
|
||||
window_str = f"{goal.eating_window_start_hour:02d}:00–{goal.eating_window_end_hour:02d}:00"
|
||||
|
|
@ -352,7 +353,7 @@ async def get_electrolytes_today(
|
|||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
today_start = datetime.combine(date.today(), datetime.min.time()).replace(tzinfo=timezone.utc)
|
||||
today_start = day_start_utc(user.timezone)
|
||||
result = await db.execute(
|
||||
select(
|
||||
func.coalesce(func.sum(ElectrolyteLog.sodium_mg), 0),
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ from diligence.models.reward import Reward
|
|||
from diligence.schemas.points import PointRuleUpdate, DailyTargetUpdate
|
||||
from diligence.schemas.reward import RewardCreate, RewardRedeemRequest
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import today_for_user
|
||||
from diligence.services.points_engine import get_today_status, get_weekly_summary, redeem_reward
|
||||
|
||||
router = APIRouter(prefix="/api/points", tags=["points"])
|
||||
|
|
@ -24,7 +25,7 @@ async def today_status(
|
|||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
return await get_today_status(db, user.id)
|
||||
return await get_today_status(db, user.id, tz_str=user.timezone)
|
||||
|
||||
|
||||
@router.get("/week")
|
||||
|
|
@ -33,7 +34,7 @@ async def week_summary(
|
|||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
start: date | None = None,
|
||||
):
|
||||
d = start or date.today()
|
||||
d = start or today_for_user(user.timezone)
|
||||
return await get_weekly_summary(db, user.id, d)
|
||||
|
||||
|
||||
|
|
@ -143,7 +144,7 @@ async def redeem(
|
|||
user: Annotated[User, Depends(get_current_user)],
|
||||
db: Annotated[AsyncSession, Depends(get_db)],
|
||||
):
|
||||
result = await redeem_reward(db, user.id, uuid_mod.UUID(reward_id), req.redemption_date)
|
||||
result = await redeem_reward(db, user.id, uuid_mod.UUID(reward_id), req.redemption_date, tz_str=user.timezone)
|
||||
if not result["success"]:
|
||||
raise HTTPException(status_code=400, detail=result["reason"])
|
||||
return result
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ from diligence.models.user import User
|
|||
from diligence.models.program import Program
|
||||
from diligence.models.activity import ActivityLog
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import today_for_user
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api/programs", tags=["programs"])
|
||||
|
|
@ -37,7 +38,7 @@ async def list_programs(
|
|||
programs = result.scalars().all()
|
||||
out = []
|
||||
for p in programs:
|
||||
today = date.today()
|
||||
today = today_for_user(user.timezone)
|
||||
day_num = (today - p.start_date).days + 1
|
||||
total = (p.end_date - p.start_date).days + 1
|
||||
out.append({
|
||||
|
|
@ -90,7 +91,7 @@ async def get_program(
|
|||
)
|
||||
logged = days_logged.scalar() or 0
|
||||
|
||||
today = date.today()
|
||||
today = today_for_user(user.timezone)
|
||||
day_num = (today - p.start_date).days + 1
|
||||
total = (p.end_date - p.start_date).days + 1
|
||||
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ from diligence.models.support import SupportThread, SupportMessage
|
|||
from diligence.models.activity import ActivityLog
|
||||
from diligence.services.points_engine import get_active_program, get_today_status
|
||||
from diligence.utils.auth import get_current_user
|
||||
from diligence.utils.dates import day_start_utc
|
||||
from diligence.config import settings
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
|
@ -45,7 +46,7 @@ async def gather_user_context(db: AsyncSession, user: User) -> dict:
|
|||
}
|
||||
|
||||
try:
|
||||
program = await get_active_program(db, user.id)
|
||||
program = await get_active_program(db, user.id, tz_str=user.timezone)
|
||||
if program:
|
||||
context["program_name"] = program.get("name")
|
||||
context["program_day"] = program.get("day")
|
||||
|
|
@ -57,7 +58,7 @@ async def gather_user_context(db: AsyncSession, user: User) -> dict:
|
|||
pass
|
||||
|
||||
try:
|
||||
status = await get_today_status(db, user.id)
|
||||
status = await get_today_status(db, user.id, tz_str=user.timezone)
|
||||
context["points_today"] = status.get("points_earned", 0)
|
||||
context["daily_target"] = status.get("daily_minimum", 80)
|
||||
context["gate_passed"] = status.get("gate_passed", False)
|
||||
|
|
@ -219,7 +220,7 @@ async def send_message(
|
|||
raise HTTPException(status_code=400, detail="Message too long (max 2000 characters)")
|
||||
|
||||
# Rate limit check
|
||||
today_start = datetime.combine(date.today(), datetime.min.time(), tzinfo=timezone.utc)
|
||||
today_start = day_start_utc(user.timezone)
|
||||
thread = await get_or_create_thread(db, user.id)
|
||||
|
||||
result = await db.execute(
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ class RegisterRequest(BaseModel):
|
|||
password: str = Field(min_length=8, max_length=128)
|
||||
display_name: str = Field(min_length=1, max_length=100)
|
||||
email: str | None = None
|
||||
timezone: str | None = None
|
||||
|
||||
|
||||
class TokenResponse(BaseModel):
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ async def get_available_rewards(db: AsyncSession, user_id: uuid.UUID) -> list[di
|
|||
]
|
||||
|
||||
|
||||
async def get_active_program(db: AsyncSession, user_id: uuid.UUID) -> dict | None:
|
||||
async def get_active_program(db: AsyncSession, user_id: uuid.UUID, tz_str: str = "UTC") -> dict | None:
|
||||
result = await db.execute(
|
||||
select(Program)
|
||||
.where(Program.user_id == user_id, Program.status == "active")
|
||||
|
|
@ -90,7 +90,7 @@ async def get_active_program(db: AsyncSession, user_id: uuid.UUID) -> dict | Non
|
|||
program = result.scalar_one_or_none()
|
||||
if not program:
|
||||
return None
|
||||
today = date.today()
|
||||
today = today_for_user(tz_str)
|
||||
day_num = (today - program.start_date).days + 1
|
||||
total = (program.end_date - program.start_date).days + 1
|
||||
return {
|
||||
|
|
@ -104,8 +104,8 @@ async def get_active_program(db: AsyncSession, user_id: uuid.UUID) -> dict | Non
|
|||
}
|
||||
|
||||
|
||||
async def get_today_status(db: AsyncSession, user_id: uuid.UUID) -> dict:
|
||||
today = date.today()
|
||||
async def get_today_status(db: AsyncSession, user_id: uuid.UUID, tz_str: str = "UTC") -> dict:
|
||||
today = today_for_user(tz_str)
|
||||
earned = await get_daily_points_earned(db, user_id, today)
|
||||
target = await get_daily_target(db, user_id)
|
||||
daily_min = target.daily_minimum_pts if target else 80
|
||||
|
|
@ -189,9 +189,9 @@ async def log_activity_with_points(
|
|||
|
||||
async def redeem_reward(
|
||||
db: AsyncSession, user_id: uuid.UUID, reward_id: uuid.UUID, redemption_date: date | None = None
|
||||
) -> dict:
|
||||
, tz_str: str = "UTC") -> dict:
|
||||
"""Attempt to redeem a reward. Returns success/failure with details."""
|
||||
d = redemption_date or date.today()
|
||||
d = redemption_date or today_for_user(tz_str)
|
||||
earned = await get_daily_points_earned(db, user_id, d)
|
||||
target = await get_daily_target(db, user_id)
|
||||
daily_min = target.daily_minimum_pts if target else 80
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import annotations
|
|||
"""Polar AccessLink API client for activity sync."""
|
||||
import uuid
|
||||
from datetime import datetime, timezone
|
||||
from diligence.utils.dates import today_for_user
|
||||
import httpx
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -108,7 +109,7 @@ async def sync_polar_activities(db: AsyncSession, user_id: uuid.UUID) -> list[di
|
|||
activity_date = datetime.fromisoformat(start).date()
|
||||
except (ValueError, TypeError):
|
||||
from datetime import date
|
||||
activity_date = date.today()
|
||||
activity_date = today_for_user()
|
||||
|
||||
# Parse duration ISO 8601 (e.g., PT1H30M)
|
||||
duration_str = ex.get("duration", "")
|
||||
|
|
|
|||
|
|
@ -1,20 +1,42 @@
|
|||
"""Timezone-aware date helpers.
|
||||
|
||||
All date calculations should use these helpers instead of date.today()
|
||||
so the result reflects the user's local date, not the server's.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, timedelta
|
||||
from datetime import date, datetime, timezone
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
|
||||
def get_week_boundaries(d: date) -> tuple[date, date]:
|
||||
"""Return Monday and Sunday of the week containing date d."""
|
||||
monday = d - timedelta(days=d.weekday())
|
||||
sunday = monday + timedelta(days=6)
|
||||
return monday, sunday
|
||||
def today_for_user(tz_str: str = "UTC") -> date:
|
||||
"""Return today's date in the user's timezone."""
|
||||
try:
|
||||
tz = ZoneInfo(tz_str)
|
||||
except (KeyError, Exception):
|
||||
tz = ZoneInfo("UTC")
|
||||
return datetime.now(tz).date()
|
||||
|
||||
|
||||
def get_month_boundaries(year: int, month: int) -> tuple[date, date]:
|
||||
"""Return first and last day of a month."""
|
||||
first = date(year, month, 1)
|
||||
if month == 12:
|
||||
last = date(year + 1, 1, 1) - timedelta(days=1)
|
||||
else:
|
||||
last = date(year, month + 1, 1) - timedelta(days=1)
|
||||
return first, last
|
||||
def now_for_user(tz_str: str = "UTC") -> datetime:
|
||||
"""Return the current datetime in the user's timezone (tz-aware)."""
|
||||
try:
|
||||
tz = ZoneInfo(tz_str)
|
||||
except (KeyError, Exception):
|
||||
tz = ZoneInfo("UTC")
|
||||
return datetime.now(tz)
|
||||
|
||||
|
||||
def day_start_utc(tz_str: str = "UTC") -> datetime:
|
||||
"""Return the start of today (midnight) in the user's timezone, converted to UTC.
|
||||
|
||||
Useful for database queries that store timestamps in UTC but need
|
||||
to filter by the user's local day.
|
||||
"""
|
||||
user_today = today_for_user(tz_str)
|
||||
try:
|
||||
tz = ZoneInfo(tz_str)
|
||||
except (KeyError, Exception):
|
||||
tz = ZoneInfo("UTC")
|
||||
local_midnight = datetime(user_today.year, user_today.month, user_today.day, tzinfo=tz)
|
||||
return local_midnight.astimezone(ZoneInfo("UTC"))
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@
|
|||
<meta name="theme-color" content="#1a1a2e" />
|
||||
<meta name="apple-mobile-web-app-capable" content="yes" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<title>Fitness Rewards</title>
|
||||
<title>Diligence</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
|
|
|
|||
1865
frontend/package-lock.json
generated
Normal file
1865
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -30,7 +30,7 @@ export const api = {
|
|||
login: (username, password) =>
|
||||
request('/auth/login', { method: 'POST', body: JSON.stringify({ username, password }) }),
|
||||
register: (username, password, display_name) =>
|
||||
request('/auth/register', { method: 'POST', body: JSON.stringify({ username, password, display_name }) }),
|
||||
request('/auth/register', { method: 'POST', body: JSON.stringify({ username, password, display_name, timezone: Intl.DateTimeFormat().resolvedOptions().timeZone }) }),
|
||||
me: () => request('/auth/me'),
|
||||
|
||||
// Onboarding
|
||||
|
|
|
|||
|
|
@ -36,12 +36,12 @@ export default function Login() {
|
|||
<div style={{ width: '100%', maxWidth: '380px' }}>
|
||||
{/* Brand */}
|
||||
<div style={{ textAlign: 'center', marginBottom: '32px', color: '#fff' }}>
|
||||
<div style={{ fontSize: '3.2rem', marginBottom: '8px', filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.2))' }}>🔥</div>
|
||||
<div style={{ fontSize: '3.2rem', marginBottom: '8px', filter: 'drop-shadow(0 4px 12px rgba(0,0,0,0.2))' }}>💪</div>
|
||||
<h1 style={{ fontFamily: 'var(--font-display)', fontSize: '2.2rem', fontWeight: 900, letterSpacing: '-0.03em' }}>
|
||||
Fitness Rewards
|
||||
Diligence
|
||||
</h1>
|
||||
<p style={{ opacity: 0.8, marginTop: '4px', fontSize: '0.95rem', fontWeight: 500 }}>
|
||||
Earn your rewards. Every single day.
|
||||
Your fitness. Your data. Your rewards.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue