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 (
+ <>
+
+
+
+ >
+ );
+}
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) {