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:
parent
4e8fe2a91c
commit
711b5d838a
Binary file not shown.
|
Before Width: | Height: | Size: 413 KiB After Width: | Height: | Size: 364 KiB |
BIN
public/icons/icon-192.png
Normal file
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 |
@ -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"
|
||||
|
||||
@ -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",
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@ -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'}`}>
|
||||
"One day at a time..."
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats and Graph */}
|
||||
<div className="grid gap-6 md:grid-cols-2">
|
||||
<div className="opacity-0 animate-fade-in-up delay-200">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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')}>
|
||||
|
||||
@ -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: {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user