feat: v1.0 polish - new icon, optimized video, better buttons, and updated messaging

- Replaced app icon with new cleaner design and added PWA cache busting
- Optimized background video for better mobile performance (reduced blur/opacity)
- Increased button touch targets for better mobile accessibility
- Updated Dashboard messages and graph labels to be more supportive/less celebratory of usage
- Updated VersionUpdateModal to include fresh look announcement
This commit is contained in:
Avery Felts 2026-02-01 02:31:58 -07:00
parent 4e8fe2a91c
commit 711b5d838a
10 changed files with 99 additions and 46 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 364 KiB

BIN
public/icons/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 413 KiB

After

Width:  |  Height:  |  Size: 364 KiB

View File

@ -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"

View File

@ -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",
},
};

View File

@ -78,22 +78,15 @@ export function SubstanceTrackingPage({ user, substance }: SubstanceTrackingPage
<div className="mb-6 sm:mb-8 text-center opacity-0 animate-fade-in delay-100">
{todayCount === 0 ? (
<p className={`text-xl sm:text-2xl font-medium ${theme === 'light' ? 'text-green-600' : 'text-green-400'}`}>
Great job, nothing yet!
0 {unitLabel} recorded, amazing job so far!
</p>
) : (
<p className={`text-xl sm:text-2xl font-medium ${theme === 'light' ? 'text-gray-900' : 'text-white'}`}>
{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.
</p>
)}
</div>
{/* Inspirational Message */}
<div className="mb-6 sm:mb-8 text-center opacity-0 animate-fade-in delay-200">
<p className={`text-lg sm:text-xl font-light italic ${theme === 'light' ? 'text-gray-500' : 'text-white/60'}`}>
&quot;One day at a time...&quot;
</p>
</div>
{/* Stats and Graph */}
<div className="grid gap-6 md:grid-cols-2">
<div className="opacity-0 animate-fade-in-up delay-200">

View File

@ -122,9 +122,11 @@ export function UsageTrendGraph({ usageData, substance }: UsageTrendGraphProps)
<p className="text-sm text-muted-foreground">Daily Average</p>
</div>
<div className="bg-muted/50 p-3 rounded-lg text-center hover:bg-muted/70 transition-all duration-200 hover:scale-[1.02]">
<p className="text-2xl font-bold capitalize">{trend}</p>
<p className="text-2xl font-bold capitalize">
{trend === 'increasing' ? 'Usage Rising' : trend === 'decreasing' ? 'Dropping' : 'Stable'}
</p>
<p className="text-sm text-muted-foreground">
{trend === 'decreasing' ? 'Great progress!' : trend === 'increasing' ? 'Stay strong!' : 'Holding steady'}
{trend === 'decreasing' ? 'Great progress!' : trend === 'increasing' ? 'Time to refocus' : 'Holding steady'}
</p>
</div>
</div>

View File

@ -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 */}
<div
className="absolute inset-0 transition-colors duration-500"
style={{
background: theme === 'light' ? 'rgba(255, 255, 255, 0.4)' : 'rgba(10, 10, 20, 0.35)',
backdropFilter: 'blur(20px)',
WebkitBackdropFilter: 'blur(20px)',
background: theme === 'light'
? (isMobile ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255, 255, 255, 0.35)')
: (isMobile ? 'rgba(10, 10, 20, 0.45)' : 'rgba(10, 10, 20, 0.35)'),
backdropFilter: isMobile ? 'blur(10px)' : 'blur(20px)', // Reduce blur on mobile for performance
WebkitBackdropFilter: isMobile ? 'blur(10px)' : 'blur(20px)',
}}
/>
@ -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]"
)}
>
<source src="/videos/smoke.mp4" type="video/mp4" />
@ -345,7 +382,12 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
</div>
{/* Vignette/Readability Overlay - Reinforced for contrast */}
<div className="absolute inset-0 bg-gradient-to-b from-black/30 via-transparent to-black/50 dark:from-black/60 dark:via-transparent dark:to-black/70 mix-blend-multiply" />
<div className={cn(
"absolute inset-0 bg-gradient-to-b mix-blend-multiply",
isMobile
? "from-black/40 via-transparent to-black/60 dark:from-black/70 dark:via-transparent dark:to-black/80" // Stronger contrast on mobile
: "from-black/30 via-transparent to-black/50 dark:from-black/60 dark:via-transparent dark:to-black/70"
)} />
</div>
<div className="container mx-auto px-4 h-16 sm:h-20 flex items-center justify-between relative z-50">

View File

@ -67,6 +67,22 @@ export function VersionUpdateModal() {
</div>
</div>
{/* PWA Icon & Look */}
<div className={cn("flex gap-4 p-4 rounded-2xl border", theme === 'light' ? 'bg-indigo-50 border-indigo-100' : 'bg-indigo-500/10 border-indigo-500/10')}>
<div className="p-2.5 bg-indigo-500/10 rounded-xl h-fit">
<Sparkles className="w-5 h-5 text-indigo-500" />
</div>
<div className="space-y-2">
<h4 className="font-bold text-sm">Fresh New Look</h4>
<p className="text-xs opacity-70 leading-relaxed">
We've updated the app icon to be cleaner and more modern.
</p>
<div className={cn("text-[10px] p-2 rounded-lg border", theme === 'light' ? 'bg-yellow-50 border-yellow-200 text-yellow-800' : 'bg-yellow-500/10 border-yellow-500/20 text-yellow-200')}>
<strong>iOS Users:</strong> 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).
</div>
</div>
</div>
{/* Notifications & Input */}
<div className="grid sm:grid-cols-2 gap-4">
<div className={cn("p-4 rounded-2xl border space-y-2", theme === 'light' ? 'bg-slate-50 border-slate-100' : 'bg-white/5 border-white/5')}>

View File

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