diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png new file mode 100644 index 0000000..1c62a48 Binary files /dev/null and b/public/icons/apple-touch-icon.png differ diff --git a/public/icons/icon-512.png b/public/icons/icon-512.png new file mode 100644 index 0000000..1c62a48 Binary files /dev/null and b/public/icons/icon-512.png differ diff --git a/public/manifest.json b/public/manifest.json new file mode 100644 index 0000000..3c645fd --- /dev/null +++ b/public/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "QuitTraq - Track Your Progress", + "short_name": "QuitTraq", + "description": "Track your nicotine and marijuana usage to quit smoking", + "start_url": "/", + "display": "standalone", + "background_color": "#0a0a0f", + "theme_color": "#8b5cf6", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + } + ], + "categories": [ + "health", + "lifestyle", + "medical" + ] +} \ No newline at end of file diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 630e5e4..d391769 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,10 +1,27 @@ -import type { Metadata } from "next"; +import type { Metadata, Viewport } from "next"; import "./globals.css"; import { Providers } from "@/components/Providers"; export const metadata: Metadata = { title: "QuitTraq - Track Your Journey to Quit Smoking", description: "Track and manage your smoking habits, set goals, and quit safely with personalized plans.", + manifest: "/manifest.json", + appleWebApp: { + capable: true, + statusBarStyle: "black-translucent", + title: "QuitTraq", + }, + icons: { + apple: "/icons/apple-touch-icon.png", + }, +}; + +export const viewport: Viewport = { + themeColor: "#8b5cf6", + width: "device-width", + initialScale: 1, + maximumScale: 1, + userScalable: false, }; export default function RootLayout({ @@ -18,6 +35,9 @@ export default function RootLayout({ + + + {children} diff --git a/src/components/DailyInspirationCard.tsx b/src/components/DailyInspirationCard.tsx index 62de9ea..3d0f277 100644 --- a/src/components/DailyInspirationCard.tsx +++ b/src/components/DailyInspirationCard.tsx @@ -11,7 +11,7 @@ import { } from '@/components/ui/dropdown-menu'; import { savePreferencesAsync, getPreferences, UserPreferences } from '@/lib/storage'; -type Religion = 'christian' | 'muslim' | 'jewish' | 'secular'; +type Religion = 'christian' | 'secular'; interface Verse { text: string; @@ -41,50 +41,6 @@ const VERSES: Record = { { text: "Therefore if any man be in Christ, he is a new creature: old things are passed away; behold, all things are become new.", source: "2 Corinthians 5:17 (KJV)" }, { text: "The LORD shall fight for you, and ye shall hold your peace.", source: "Exodus 14:14 (KJV)" }, ], - muslim: [ - { text: "Indeed, with hardship [will be] ease.", source: "Surah Ash-Sharh 94:6" }, - { text: "Allah does not burden a soul beyond that it can bear.", source: "Surah Al-Baqarah 2:286" }, - { text: "So remember Me; I will remember you.", source: "Surah Al-Baqarah 2:152" }, - { text: "And whosoever fears Allah... He will make a way for him to get out (from every difficulty).", source: "Surah At-Talaq 65:2" }, - { text: "Indeed, Allah is with the patient.", source: "Surah Al-Baqarah 2:153" }, - { text: "Call upon Me; I will respond to you.", source: "Surah Ghafir 40:60" }, - { text: "And He found you lost and guided [you].", source: "Surah Ad-Duhaa 93:7" }, - { text: "Unquestionably, by the remembrance of Allah hearts are assured.", source: "Surah Ar-Ra'd 13:28" }, - { text: "And put your trust in Allah, and sufficient is Allah as a Disposer of affairs.", source: "Surah Al-Ahzab 33:3" }, - { text: "Verily, in the remembrance of Allah do hearts find rest.", source: "Surah Ar-Ra'd 13:28" }, - { text: "Our Lord, pour upon us patience and plant firmly our feet.", source: "Surah Al-Baqarah 2:250" }, - { text: "If Allah should aid you, no one can overcome you.", source: "Surah Ali 'Imran 3:160" }, - { text: "Indeed, my Lord is near and responsive.", source: "Surah Hud 11:61" }, - { text: "And seek help through patience and prayer, and indeed, it is difficult except for the humbly submissive.", source: "Surah Al-Baqarah 2:45" }, - { text: "So surely with hardship comes ease.", source: "Surah Ash-Sharh 94:5" }, - { text: "Do not despair of the mercy of Allah.", source: "Surah Az-Zumar 39:53" }, - { text: "He knows what is in every heart.", source: "Surah Al-Mulk 67:13" }, - { text: "Allah is the best of planners.", source: "Surah Al-Anfal 8:30" }, - { text: "My Lord, indeed I am, for whatever good You would send down to me, in need.", source: "Surah Al-Qasas 28:24" }, - { text: "Indeed, good deeds do away with misdeeds.", source: "Surah Hud 11:114" }, - ], - jewish: [ - { text: "Be strong and of good courage.", source: "Joshua 1:9 (Tanakh)" }, - { text: "I have set the LORD always before me; because He is at my right hand, I shall not be moved.", source: "Psalm 16:8 (Tanakh)" }, - { text: "The LORD is my light and my salvation; whom shall I fear?", source: "Psalm 27:1 (Tanakh)" }, - { text: "Trust in the LORD with all your heart, and do not rely on your own understanding.", source: "Proverbs 3:5 (Tanakh)" }, - { text: "Create in me a clean heart, O God, and put a new and right spirit within me.", source: "Psalm 51:10 (Tanakh)" }, - { text: "Where there is no vision, the people perish.", source: "Proverbs 29:18 (Tanakh)" }, - { text: "A righteous man falls seven times, and rises up again.", source: "Proverbs 24:16 (Tanakh)" }, - { text: "Do not fear, for I am with you; do not be dismayed, for I am your God.", source: "Isaiah 41:10 (Tanakh)" }, - { text: "As water reflects the face, so one’s life reflects the heart.", source: "Proverbs 27:19 (Tanakh)" }, - { text: "If I am not for myself, who will be for me? And if I am only for myself, what am I?", source: "Pirkei Avot 1:14" }, - { text: "The world stands on three things: Torah, service, and acts of loving kindness.", source: "Pirkei Avot 1:2" }, - { text: "According to the effort is the reward.", source: "Pirkei Avot 5:23" }, - { text: "It is not your duty to finish the work, but neither are you at liberty to neglect it.", source: "Pirkei Avot 2:16" }, - { text: "Who is wise? One who learns from every man. Who is strong? One who overpowers his inclinations.", source: "Pirkei Avot 4:1" }, - { text: "Even in laughter the heart may ache, and the end of joy may be grief.", source: "Proverbs 14:13 (Tanakh)" }, - { text: "A soft answer turns away wrath, but a harsh word stirs up anger.", source: "Proverbs 15:1 (Tanakh)" }, - { text: "He who walks with integrity walks securely.", source: "Proverbs 10:9 (Tanakh)" }, - { text: "The candle of God is the soul of man.", source: "Proverbs 20:27 (Tanakh)" }, - { text: "There is a time for everything, and a season for every activity under the heavens.", source: "Ecclesiastes 3:1 (Tanakh)" }, - { text: "What you hate, do not do to your neighbor.", source: "Hillel the Elder (Shabbat 31a)" }, - ], secular: [ { text: "The only way to do great work is to love what you do.", source: "Steve Jobs" }, { text: "It is during our darkest moments that we must focus to see the light.", source: "Aristotle" }, @@ -148,44 +104,30 @@ export function DailyInspirationCard({ initialReligion, onReligionChange }: Dail const getLabel = (r: Religion) => { switch (r) { - case 'christian': return 'Christian (Bible)'; - case 'muslim': return 'Muslim (Quran)'; - case 'jewish': return 'Jewish (Torah)'; + case 'christian': return 'Christian (KJV)'; case 'secular': return 'Secular (Quotes)'; } }; const getIcon = () => { - switch (currentReligion) { - case 'christian': - case 'muslim': - case 'jewish': - return ; - default: - return ; + if (currentReligion === 'christian') { + return ; } + return ; }; const getBackground = () => { - switch (currentReligion) { - case 'christian': - return 'linear-gradient(135deg, rgba(59, 130, 246, 0.35) 0%, rgba(37, 99, 235, 0.3) 50%, rgba(30, 64, 175, 0.4) 100%)'; // Blue - case 'muslim': - return 'linear-gradient(135deg, rgba(16, 185, 129, 0.35) 0%, rgba(5, 150, 105, 0.3) 50%, rgba(4, 120, 87, 0.4) 100%)'; // Emerald - case 'jewish': - return 'linear-gradient(135deg, rgba(147, 51, 234, 0.35) 0%, rgba(126, 34, 206, 0.3) 50%, rgba(107, 33, 168, 0.4) 100%)'; // Purple - default: - return 'linear-gradient(135deg, rgba(67, 56, 202, 0.35) 0%, rgba(109, 40, 217, 0.3) 50%, rgba(76, 29, 149, 0.4) 100%)'; // Indigo/Original + if (currentReligion === 'christian') { + return 'linear-gradient(135deg, rgba(59, 130, 246, 0.35) 0%, rgba(37, 99, 235, 0.3) 50%, rgba(30, 64, 175, 0.4) 100%)'; // Blue } + return 'linear-gradient(135deg, rgba(67, 56, 202, 0.35) 0%, rgba(109, 40, 217, 0.3) 50%, rgba(76, 29, 149, 0.4) 100%)'; // Indigo/Original }; const getShadowColor = () => { - switch (currentReligion) { - case 'christian': return 'rgba(59, 130, 246, 0.15)'; - case 'muslim': return 'rgba(16, 185, 129, 0.15)'; - case 'jewish': return 'rgba(147, 51, 234, 0.15)'; - default: return 'rgba(99, 102, 241, 0.15)'; + if (currentReligion === 'christian') { + return 'rgba(59, 130, 246, 0.15)'; } + return 'rgba(99, 102, 241, 0.15)'; }; return ( @@ -218,7 +160,7 @@ export function DailyInspirationCard({ initialReligion, onReligionChange }: Dail - {(['christian', 'muslim', 'jewish', 'secular'] as Religion[]).map((r) => ( + {(['christian', 'secular'] as Religion[]).map((r) => ( handleReligionChange(r)} diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index 4d75a9e..8a76e51 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -118,7 +118,7 @@ export function Dashboard({ user }: DashboardProps) { init(); }, [loadData, checkAndUnlockAchievements]); - const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => { + const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'secular' }) => { const today = getTodayString(); const newPrefs: UserPreferences = { substance: data.substance, diff --git a/src/components/InstallAppButton.tsx b/src/components/InstallAppButton.tsx new file mode 100644 index 0000000..d6dcec7 --- /dev/null +++ b/src/components/InstallAppButton.tsx @@ -0,0 +1,180 @@ +'use client'; + +import { useState, useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { Download, Share, Plus, MoreVertical, Smartphone } from 'lucide-react'; + +interface BeforeInstallPromptEvent extends Event { + prompt: () => Promise; + userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>; +} + +export function InstallAppButton() { + const [showInstructions, setShowInstructions] = useState(false); + const [deferredPrompt, setDeferredPrompt] = useState(null); + const [isIOS, setIsIOS] = useState(false); + const [isAndroid, setIsAndroid] = useState(false); + const [isStandalone, setIsStandalone] = useState(false); + + useEffect(() => { + // Check if already installed as PWA + const isAppInstalled = window.matchMedia('(display-mode: standalone)').matches || + (window.navigator as Navigator & { standalone?: boolean }).standalone === true; + setIsStandalone(isAppInstalled); + + // Detect platform + const userAgent = window.navigator.userAgent.toLowerCase(); + const isIOSDevice = /iphone|ipad|ipod/.test(userAgent); + const isAndroidDevice = /android/.test(userAgent); + setIsIOS(isIOSDevice); + setIsAndroid(isAndroidDevice); + + // Listen for beforeinstallprompt (Android/Chrome) + const handleBeforeInstallPrompt = (e: Event) => { + e.preventDefault(); + setDeferredPrompt(e as BeforeInstallPromptEvent); + }; + + window.addEventListener('beforeinstallprompt', handleBeforeInstallPrompt); + + return () => { + window.removeEventListener('beforeinstallprompt', handleBeforeInstallPrompt); + }; + }, []); + + const handleInstallClick = async () => { + if (deferredPrompt) { + // Use the native install prompt (Android/Chrome) + await deferredPrompt.prompt(); + const { outcome } = await deferredPrompt.userChoice; + if (outcome === 'accepted') { + setDeferredPrompt(null); + } + } else { + // Show manual instructions + setShowInstructions(true); + } + }; + + // Don't show if already installed as PWA + if (isStandalone) { + return null; + } + + return ( + <> + + + + + + + + Add QuitTraq to Home Screen + + + Get quick access to track your progress right from your phone's home screen. + + + +
+ {isIOS ? ( + // iOS Instructions +
+

For iPhone / iPad (Safari):

+
    +
  1. + 1 +
    +

    Tap the Share button

    +
    + +
    +
    +
  2. +
  3. + 2 +
    +

    Scroll down and tap "Add to Home Screen"

    +
    + Add to Home Screen +
    +
    +
  4. +
  5. + 3 +

    Tap "Add" to confirm

    +
  6. +
+
+ ) : isAndroid ? ( + // Android Instructions +
+

For Android (Chrome):

+
    +
  1. + 1 +
    +

    Tap the menu button (3 dots)

    +
    + +
    +
    +
  2. +
  3. + 2 +
    +

    Tap "Add to Home screen" or "Install app"

    +
    +
  4. +
  5. + 3 +

    Tap "Add" or "Install" to confirm

    +
  6. +
+
+ ) : ( + // Desktop / Unknown +
+

On mobile device:

+
    +
  • iPhone/iPad: Use Safari, tap Share → Add to Home Screen
  • +
  • Android: Use Chrome, tap Menu → Add to Home screen
  • +
+

+ Open this page on your phone to add QuitTraq to your home screen for quick access! +

+
+ )} + +
+

+ 📱 Once added, tap the QuitTraq icon to quickly log your usage! +

+
+
+ + +
+
+ + ); +} diff --git a/src/components/SetupWizard.tsx b/src/components/SetupWizard.tsx index ed66334..6abbb2b 100644 --- a/src/components/SetupWizard.tsx +++ b/src/components/SetupWizard.tsx @@ -21,7 +21,7 @@ import { interface SetupWizardProps { open: boolean; - onComplete: (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => void; + onComplete: (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'secular' }) => void; } export function SetupWizard({ open, onComplete }: SetupWizardProps) { @@ -29,7 +29,7 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) { const [name, setName] = useState(''); const [age, setAge] = useState('25'); const [substance, setSubstance] = useState<'nicotine' | 'weed' | ''>(''); - const [religion, setReligion] = useState<'christian' | 'muslim' | 'jewish' | 'secular' | ''>(''); + const [religion, setReligion] = useState<'christian' | 'secular' | ''>(''); const ages = Array.from({ length: 83 }, (_, i) => (i + 18).toString()); @@ -159,14 +159,12 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) {
- setReligion(v as 'christian' | 'secular')}> - Christian (Bible Verses) - Muslim (Quran Verses) - Jewish (Torah/Tanakh) + Christian (KJV Bible Verses) Secular (Motivational Quotes) diff --git a/src/components/UsageCalendar.tsx b/src/components/UsageCalendar.tsx index f543b66..4652195 100644 --- a/src/components/UsageCalendar.tsx +++ b/src/components/UsageCalendar.tsx @@ -24,8 +24,8 @@ interface UsageCalendarProps { usageData: UsageEntry[]; onDataUpdate: () => void; userId: string; - religion?: 'christian' | 'muslim' | 'jewish' | 'secular' | null; - onReligionUpdate?: (religion: 'christian' | 'muslim' | 'jewish' | 'secular') => void; + religion?: 'christian' | 'secular' | null; + onReligionUpdate?: (religion: 'christian' | 'secular') => void; preferences?: UserPreferences | null; onPreferencesUpdate?: (prefs: UserPreferences) => Promise; } diff --git a/src/components/UserHeader.tsx b/src/components/UserHeader.tsx index df9e17b..8216e89 100644 --- a/src/components/UserHeader.tsx +++ b/src/components/UserHeader.tsx @@ -24,6 +24,7 @@ import { useEffect, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Cigarette, Leaf, LogOut, Home, ChevronDown, Sun, Moon, Bell, BellOff, BellRing, Menu } from 'lucide-react'; import { useTheme } from '@/lib/theme-context'; +import { InstallAppButton } from './InstallAppButton'; interface UserHeaderProps { user: User; @@ -132,6 +133,7 @@ export function UserHeader({ user, preferences }: UserHeaderProps) { )} +