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: {