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",
|
"orientation": "portrait-primary",
|
||||||
"icons": [
|
"icons": [
|
||||||
{
|
{
|
||||||
"src": "/icons/icon-192.png",
|
"src": "/icons/icon-192.png?v=2",
|
||||||
"sizes": "192x192",
|
"sizes": "192x192",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"src": "/icons/icon-512.png",
|
"src": "/icons/icon-512.png?v=2",
|
||||||
"sizes": "512x512",
|
"sizes": "512x512",
|
||||||
"type": "image/png",
|
"type": "image/png",
|
||||||
"purpose": "any maskable"
|
"purpose": "any maskable"
|
||||||
|
|||||||
@ -12,7 +12,7 @@ export const metadata: Metadata = {
|
|||||||
title: "QuitTraq",
|
title: "QuitTraq",
|
||||||
},
|
},
|
||||||
icons: {
|
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">
|
<div className="mb-6 sm:mb-8 text-center opacity-0 animate-fade-in delay-100">
|
||||||
{todayCount === 0 ? (
|
{todayCount === 0 ? (
|
||||||
<p className={`text-xl sm:text-2xl font-medium ${theme === 'light' ? 'text-green-600' : 'text-green-400'}`}>
|
<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>
|
||||||
) : (
|
) : (
|
||||||
<p className={`text-xl sm:text-2xl font-medium ${theme === 'light' ? 'text-gray-900' : 'text-white'}`}>
|
<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>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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 */}
|
{/* Stats and Graph */}
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
<div className="opacity-0 animate-fade-in-up delay-200">
|
<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>
|
<p className="text-sm text-muted-foreground">Daily Average</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-muted/50 p-3 rounded-lg text-center hover:bg-muted/70 transition-all duration-200 hover:scale-[1.02]">
|
<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">
|
<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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -137,25 +137,58 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
|
|||||||
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
|
||||||
const [isVideoPlaying, setIsVideoPlaying] = 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
|
// Force play background video
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (videoRef.current) {
|
if (videoRef.current) {
|
||||||
// Programmatic attributes for iOS Safari / PWA compatibility
|
const video = videoRef.current;
|
||||||
videoRef.current.muted = true;
|
|
||||||
videoRef.current.defaultMuted = true;
|
// Optimization: For Desktop, we prioritize quality logic if we had multiple sources.
|
||||||
videoRef.current.playsInline = true;
|
// For now, we ensure robust playback for both.
|
||||||
videoRef.current.loop = true;
|
|
||||||
|
// 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 already ready, set loaded immediately
|
||||||
if (videoRef.current.readyState >= 3) {
|
if (video.readyState >= 3) {
|
||||||
setIsVideoLoaded(true);
|
setIsVideoLoaded(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
videoRef.current.play().catch((err) => {
|
// Try playing
|
||||||
|
const playPromise = video.play();
|
||||||
|
if (playPromise !== undefined) {
|
||||||
|
playPromise.catch((err) => {
|
||||||
console.warn("Autoplay failed, user interaction might be needed:", 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(() => {
|
useEffect(() => {
|
||||||
if (onModalStateChange) {
|
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
|
<div
|
||||||
className="absolute inset-0 transition-colors duration-500"
|
className="absolute inset-0 transition-colors duration-500"
|
||||||
style={{
|
style={{
|
||||||
background: theme === 'light' ? 'rgba(255, 255, 255, 0.4)' : 'rgba(10, 10, 20, 0.35)',
|
background: theme === 'light'
|
||||||
backdropFilter: 'blur(20px)',
|
? (isMobile ? 'rgba(255, 255, 255, 0.45)' : 'rgba(255, 255, 255, 0.35)')
|
||||||
WebkitBackdropFilter: 'blur(20px)',
|
: (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"
|
src="/videos/smoke-poster.jpg"
|
||||||
alt=""
|
alt=""
|
||||||
className={cn(
|
className={cn(
|
||||||
"absolute inset-0 w-full h-full object-cover scale-110 transition-opacity duration-1000",
|
"absolute inset-0 w-full h-full object-cover transition-opacity duration-1000",
|
||||||
isVideoPlaying ? "opacity-30" : "opacity-70 dark:opacity-50"
|
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"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
@ -334,10 +370,11 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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
|
isVideoPlaying
|
||||||
? "opacity-70 dark:opacity-50"
|
? (isMobile ? "opacity-60 dark:opacity-40" : "opacity-80 dark:opacity-60") // Lower opacity on mobile for better text contrast
|
||||||
: "opacity-[0.01]" // Keep very slightly visible so OS doesn't throttle it
|
: "opacity-[0.01]"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<source src="/videos/smoke.mp4" type="video/mp4" />
|
<source src="/videos/smoke.mp4" type="video/mp4" />
|
||||||
@ -345,7 +382,12 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Vignette/Readability Overlay - Reinforced for contrast */}
|
{/* 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>
|
||||||
|
|
||||||
<div className="container mx-auto px-4 h-16 sm:h-20 flex items-center justify-between relative z-50">
|
<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>
|
||||||
</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 */}
|
{/* Notifications & Input */}
|
||||||
<div className="grid sm:grid-cols-2 gap-4">
|
<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')}>
|
<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"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const buttonVariants = cva(
|
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: {
|
variants: {
|
||||||
variant: {
|
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:
|
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:
|
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",
|
"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:
|
secondary:
|
||||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
"bg-secondary text-secondary-foreground hover:bg-secondary/80 shadow-sm",
|
||||||
ghost:
|
ghost:
|
||||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||||
link: "text-primary underline-offset-4 hover:underline",
|
link: "text-primary underline-offset-4 hover:underline",
|
||||||
},
|
},
|
||||||
size: {
|
size: {
|
||||||
default: "h-9 px-4 py-2 has-[>svg]:px-3",
|
default: "h-12 px-5 py-2 has-[>svg]:px-4 text-base",
|
||||||
xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
|
xs: "h-8 gap-1 rounded-md px-3 text-xs has-[>svg]:px-2 [&_svg:not([class*='size-'])]:size-3.5",
|
||||||
sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
|
sm: "h-10 rounded-md gap-1.5 px-4 has-[>svg]:px-3",
|
||||||
lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
|
lg: "h-14 rounded-xl px-8 text-lg has-[>svg]:px-6",
|
||||||
icon: "size-9",
|
icon: "size-12 rounded-xl",
|
||||||
"icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
|
"icon-xs": "size-8 rounded-lg [&_svg:not([class*='size-'])]:size-4",
|
||||||
"icon-sm": "size-8",
|
"icon-sm": "size-10 rounded-lg",
|
||||||
"icon-lg": "size-10",
|
"icon-lg": "size-14 rounded-2xl",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
defaultVariants: {
|
defaultVariants: {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user