diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png index 1c62a48..6da22b6 100644 Binary files a/public/icons/apple-touch-icon.png and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-192.png b/public/icons/icon-192.png new file mode 100644 index 0000000..6da22b6 Binary files /dev/null and b/public/icons/icon-192.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png index 1c62a48..6da22b6 100644 Binary files a/public/icons/icon-512.png and b/public/icons/icon-512.png differ diff --git a/public/manifest.json b/public/manifest.json index 3c645fd..940f269 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -9,13 +9,13 @@ "orientation": "portrait-primary", "icons": [ { - "src": "/icons/icon-192.png", + "src": "/icons/icon-192.png?v=2", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" }, { - "src": "/icons/icon-512.png", + "src": "/icons/icon-512.png?v=2", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" diff --git a/src/app/layout.tsx b/src/app/layout.tsx index d391769..ac746eb 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -12,7 +12,7 @@ export const metadata: Metadata = { title: "QuitTraq", }, icons: { - apple: "/icons/apple-touch-icon.png", + apple: "/icons/apple-touch-icon.png?v=2", }, }; diff --git a/src/components/SubstanceTrackingPage.tsx b/src/components/SubstanceTrackingPage.tsx index 126ce42..c89ed01 100644 --- a/src/components/SubstanceTrackingPage.tsx +++ b/src/components/SubstanceTrackingPage.tsx @@ -78,22 +78,15 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
{todayCount === 0 ? (

- Great job, nothing yet! + 0 {unitLabel} recorded, amazing job so far!

) : (

- {todayCount} {todayCount === 1 ? (substance === 'nicotine' ? 'puff' : 'hit') : unitLabel} recorded, you got this! + {todayCount} {todayCount === 1 ? (substance === 'nicotine' ? 'puff' : 'hit') : unitLabel} recorded, but don't stress.

)}
- {/* Inspirational Message */} -
-

- "One day at a time..." -

-
- {/* Stats and Graph */}
diff --git a/src/components/UsageTrendGraph.tsx b/src/components/UsageTrendGraph.tsx index e8b1957..4482897 100644 --- a/src/components/UsageTrendGraph.tsx +++ b/src/components/UsageTrendGraph.tsx @@ -122,9 +122,11 @@ export function UsageTrendGraph({ usageData, substance }: UsageTrendGraphProps)

Daily Average

-

{trend}

+

+ {trend === 'increasing' ? 'Usage Rising' : trend === 'decreasing' ? 'Dropping' : 'Stable'} +

- {trend === 'decreasing' ? 'Great progress!' : trend === 'increasing' ? 'Stay strong!' : 'Holding steady'} + {trend === 'decreasing' ? 'Great progress!' : trend === 'increasing' ? 'Time to refocus' : 'Holding steady'}

diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index b49d75a..ace967e 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -137,25 +137,58 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader const [isVideoLoaded, setIsVideoLoaded] = useState(false); const [isVideoPlaying, setIsVideoPlaying] = useState(false); + const [isMobile, setIsMobile] = useState(false); + const [isPWA, setIsPWA] = useState(false); + + useEffect(() => { + // Detect Mobile/PWA + const checkMobile = () => { + const userAgent = typeof window.navigator === "undefined" ? "" : navigator.userAgent; + const mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(userAgent); + setIsMobile(mobile); + + // Detect PWA (standalone mode) + const isStandalone = window.matchMedia('(display-mode: standalone)').matches || (window.navigator as any).standalone; + setIsPWA(!!isStandalone); + }; + checkMobile(); + }, []); + // Force play background video useEffect(() => { if (videoRef.current) { - // Programmatic attributes for iOS Safari / PWA compatibility - videoRef.current.muted = true; - videoRef.current.defaultMuted = true; - videoRef.current.playsInline = true; - videoRef.current.loop = true; + const video = videoRef.current; + + // Optimization: For Desktop, we prioritize quality logic if we had multiple sources. + // For now, we ensure robust playback for both. + + // PWA/Mobile specific optimizations + if (isMobile || isPWA) { + // Ensure strictly muted/inline for iOS policy + video.muted = true; + video.defaultMuted = true; + video.playsInline = true; + video.setAttribute('playsinline', 'true'); // Explicit attribute for some older browsers + video.setAttribute('webkit-playsinline', 'true'); + } + + video.loop = true; // If already ready, set loaded immediately - if (videoRef.current.readyState >= 3) { + if (video.readyState >= 3) { setIsVideoLoaded(true); } - videoRef.current.play().catch((err) => { - console.warn("Autoplay failed, user interaction might be needed:", err); - }); + // Try playing + const playPromise = video.play(); + if (playPromise !== undefined) { + playPromise.catch((err) => { + console.warn("Autoplay failed, user interaction might be needed:", err); + // If PWA, we might want to show the poster as fallback silently without error spam + }); + } } - }, []); + }, [isMobile, isPWA]); useEffect(() => { if (onModalStateChange) { @@ -277,13 +310,15 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader } ` }} /> - {/* Base Color & Blur Layer */} + {/* Base Color & Blur Layer - Optimized for Performance on Mobile */}
@@ -303,8 +338,9 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader src="/videos/smoke-poster.jpg" alt="" className={cn( - "absolute inset-0 w-full h-full object-cover scale-110 transition-opacity duration-1000", - isVideoPlaying ? "opacity-30" : "opacity-70 dark:opacity-50" + "absolute inset-0 w-full h-full object-cover transition-opacity duration-1000", + isMobile ? "scale-105" : "scale-110", // Reduce scale on mobile + isVideoPlaying ? "opacity-0" : "opacity-100" // Hide poster completely when playing for better perf, or keep low opacity )} aria-hidden="true" /> @@ -334,10 +370,11 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader } }} className={cn( - "absolute inset-0 w-full h-full object-cover scale-110 transition-opacity duration-[1500ms] ease-in-out", + "absolute inset-0 w-full h-full object-cover transition-opacity duration-[1500ms] ease-in-out", + isMobile ? "scale-105" : "scale-110", isVideoPlaying - ? "opacity-70 dark:opacity-50" - : "opacity-[0.01]" // Keep very slightly visible so OS doesn't throttle it + ? (isMobile ? "opacity-60 dark:opacity-40" : "opacity-80 dark:opacity-60") // Lower opacity on mobile for better text contrast + : "opacity-[0.01]" )} > @@ -345,7 +382,12 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
{/* Vignette/Readability Overlay - Reinforced for contrast */} -
+
diff --git a/src/components/VersionUpdateModal.tsx b/src/components/VersionUpdateModal.tsx index f344f37..2bee79c 100644 --- a/src/components/VersionUpdateModal.tsx +++ b/src/components/VersionUpdateModal.tsx @@ -67,6 +67,22 @@ export function VersionUpdateModal() {
+ {/* PWA Icon & Look */} +
+
+ +
+
+

Fresh New Look

+

+ We've updated the app icon to be cleaner and more modern. +

+
+ iOS Users: To see the new icon, you'll need to remove the app from your home screen and re-add it (Share → Add to Home Screen). +
+
+
+ {/* Notifications & Input */}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx index 915ea2a..03fd2b5 100644 --- a/src/components/ui/button.tsx +++ b/src/components/ui/button.tsx @@ -5,30 +5,30 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const buttonVariants = cva( - "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", + "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all duration-200 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive active:scale-[0.97]", { variants: { variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", + default: "bg-primary text-primary-foreground hover:bg-primary/90 shadow-sm hover:shadow-md", destructive: - "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60 shadow-sm", outline: "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", + "bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm", ghost: "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", link: "text-primary underline-offset-4 hover:underline", }, size: { - default: "h-9 px-4 py-2 has-[>svg]:px-3", - xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", - lg: "h-10 rounded-md px-6 has-[>svg]:px-4", - icon: "size-9", - "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-8", - "icon-lg": "size-10", + default: "h-12 px-5 py-2 has-[>svg]:px-4 text-base", + xs: "h-8 gap-1 rounded-md px-3 text-xs has-[>svg]:px-2 [&_svg:not([class*='size-'])]:size-3.5", + sm: "h-10 rounded-md gap-1.5 px-4 has-[>svg]:px-3", + lg: "h-14 rounded-xl px-8 text-lg has-[>svg]:px-6", + icon: "size-12 rounded-xl", + "icon-xs": "size-8 rounded-lg [&_svg:not([class*='size-'])]:size-4", + "icon-sm": "size-10 rounded-lg", + "icon-lg": "size-14 rounded-2xl", }, }, defaultVariants: {