feat: add daily religious verse placard with user preference support
This commit is contained in:
parent
8d79f63232
commit
4ad4bd0884
4
src/app/callback/route.ts
Normal file
4
src/app/callback/route.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { handleAuth } from "@workos-inc/authkit-nextjs";
|
||||||
|
|
||||||
|
// Handle the WorkOS OAuth callback
|
||||||
|
export const GET = handleAuth({ returnPathname: "/" });
|
||||||
177
src/app/globals 2.css
Normal file
177
src/app/globals 2.css
Normal file
@ -0,0 +1,177 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
@import "tw-animate-css";
|
||||||
|
|
||||||
|
@custom-variant dark (&:is(.dark *));
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: oklch(0.9789 0.0082 121.6272);
|
||||||
|
--foreground: oklch(0 0 0);
|
||||||
|
--card: oklch(1.0000 0 0);
|
||||||
|
--card-foreground: oklch(0 0 0);
|
||||||
|
--popover: oklch(1.0000 0 0);
|
||||||
|
--popover-foreground: oklch(0 0 0);
|
||||||
|
--primary: oklch(0.5106 0.2301 276.9656);
|
||||||
|
--primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--secondary: oklch(0.7038 0.1230 182.5025);
|
||||||
|
--secondary-foreground: oklch(1.0000 0 0);
|
||||||
|
--muted: oklch(0.9551 0 0);
|
||||||
|
--muted-foreground: oklch(0.3211 0 0);
|
||||||
|
--accent: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--accent-foreground: oklch(0 0 0);
|
||||||
|
--destructive: oklch(0.6368 0.2078 25.3313);
|
||||||
|
--destructive-foreground: oklch(1.0000 0 0);
|
||||||
|
--border: oklch(0 0 0);
|
||||||
|
--input: oklch(0.5555 0 0);
|
||||||
|
--ring: oklch(0.7853 0.1041 274.7134);
|
||||||
|
--chart-1: oklch(0.5106 0.2301 276.9656);
|
||||||
|
--chart-2: oklch(0.7038 0.1230 182.5025);
|
||||||
|
--chart-3: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--chart-4: oklch(0.6559 0.2118 354.3084);
|
||||||
|
--chart-5: oklch(0.7227 0.1920 149.5793);
|
||||||
|
--sidebar: oklch(0.9789 0.0082 121.6272);
|
||||||
|
--sidebar-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-primary: oklch(0.5106 0.2301 276.9656);
|
||||||
|
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-accent: oklch(0.7686 0.1647 70.0804);
|
||||||
|
--sidebar-accent-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-border: oklch(0 0 0);
|
||||||
|
--sidebar-ring: oklch(0.7853 0.1041 274.7134);
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 0px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.05;
|
||||||
|
--shadow-color: #1a1a1a;
|
||||||
|
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||||
|
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||||
|
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
|
||||||
|
--tracking-normal: normal;
|
||||||
|
--spacing: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: oklch(0 0 0);
|
||||||
|
--foreground: oklch(1.0000 0 0);
|
||||||
|
--card: oklch(0.2455 0.0217 257.2823);
|
||||||
|
--card-foreground: oklch(1.0000 0 0);
|
||||||
|
--popover: oklch(0.2455 0.0217 257.2823);
|
||||||
|
--popover-foreground: oklch(1.0000 0 0);
|
||||||
|
--primary: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--primary-foreground: oklch(0 0 0);
|
||||||
|
--secondary: oklch(0.7845 0.1325 181.9120);
|
||||||
|
--secondary-foreground: oklch(0 0 0);
|
||||||
|
--muted: oklch(0.3211 0 0);
|
||||||
|
--muted-foreground: oklch(0.8452 0 0);
|
||||||
|
--accent: oklch(0.8790 0.1534 91.6054);
|
||||||
|
--accent-foreground: oklch(0 0 0);
|
||||||
|
--destructive: oklch(0.7106 0.1661 22.2162);
|
||||||
|
--destructive-foreground: oklch(0 0 0);
|
||||||
|
--border: oklch(0.4459 0 0);
|
||||||
|
--input: oklch(1.0000 0 0);
|
||||||
|
--ring: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--chart-1: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--chart-2: oklch(0.7845 0.1325 181.9120);
|
||||||
|
--chart-3: oklch(0.8790 0.1534 91.6054);
|
||||||
|
--chart-4: oklch(0.7253 0.1752 349.7607);
|
||||||
|
--chart-5: oklch(0.8003 0.1821 151.7110);
|
||||||
|
--sidebar: oklch(0 0 0);
|
||||||
|
--sidebar-foreground: oklch(1.0000 0 0);
|
||||||
|
--sidebar-primary: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--sidebar-primary-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-accent: oklch(0.8790 0.1534 91.6054);
|
||||||
|
--sidebar-accent-foreground: oklch(0 0 0);
|
||||||
|
--sidebar-border: oklch(1.0000 0 0);
|
||||||
|
--sidebar-ring: oklch(0.6801 0.1583 276.9349);
|
||||||
|
--radius: 1rem;
|
||||||
|
--shadow-x: 0px;
|
||||||
|
--shadow-y: 0px;
|
||||||
|
--shadow-blur: 0px;
|
||||||
|
--shadow-spread: 0px;
|
||||||
|
--shadow-opacity: 0.05;
|
||||||
|
--shadow-color: #1a1a1a;
|
||||||
|
--shadow-2xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||||
|
--shadow-xs: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.03);
|
||||||
|
--shadow-sm: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 1px 2px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-md: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 2px 4px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-lg: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 4px 6px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.05), 0px 8px 10px -1px hsl(0 0% 10.1961% / 0.05);
|
||||||
|
--shadow-2xl: 0px 0px 0px 0px hsl(0 0% 10.1961% / 0.13);
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--color-card: var(--card);
|
||||||
|
--color-card-foreground: var(--card-foreground);
|
||||||
|
--color-popover: var(--popover);
|
||||||
|
--color-popover-foreground: var(--popover-foreground);
|
||||||
|
--color-primary: var(--primary);
|
||||||
|
--color-primary-foreground: var(--primary-foreground);
|
||||||
|
--color-secondary: var(--secondary);
|
||||||
|
--color-secondary-foreground: var(--secondary-foreground);
|
||||||
|
--color-muted: var(--muted);
|
||||||
|
--color-muted-foreground: var(--muted-foreground);
|
||||||
|
--color-accent: var(--accent);
|
||||||
|
--color-accent-foreground: var(--accent-foreground);
|
||||||
|
--color-destructive: var(--destructive);
|
||||||
|
--color-destructive-foreground: var(--destructive-foreground);
|
||||||
|
--color-border: var(--border);
|
||||||
|
--color-input: var(--input);
|
||||||
|
--color-ring: var(--ring);
|
||||||
|
--color-chart-1: var(--chart-1);
|
||||||
|
--color-chart-2: var(--chart-2);
|
||||||
|
--color-chart-3: var(--chart-3);
|
||||||
|
--color-chart-4: var(--chart-4);
|
||||||
|
--color-chart-5: var(--chart-5);
|
||||||
|
--color-sidebar: var(--sidebar);
|
||||||
|
--color-sidebar-foreground: var(--sidebar-foreground);
|
||||||
|
--color-sidebar-primary: var(--sidebar-primary);
|
||||||
|
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
||||||
|
--color-sidebar-accent: var(--sidebar-accent);
|
||||||
|
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||||
|
--color-sidebar-border: var(--sidebar-border);
|
||||||
|
--color-sidebar-ring: var(--sidebar-ring);
|
||||||
|
|
||||||
|
--font-sans: 'DM Sans', sans-serif;
|
||||||
|
--font-mono: 'Space Mono', monospace;
|
||||||
|
--font-serif: 'DM Sans', sans-serif;
|
||||||
|
|
||||||
|
--radius-sm: calc(var(--radius) - 4px);
|
||||||
|
--radius-md: calc(var(--radius) - 2px);
|
||||||
|
--radius-lg: var(--radius);
|
||||||
|
--radius-xl: calc(var(--radius) + 4px);
|
||||||
|
|
||||||
|
--shadow-2xs: var(--shadow-2xs);
|
||||||
|
--shadow-xs: var(--shadow-xs);
|
||||||
|
--shadow-sm: var(--shadow-sm);
|
||||||
|
--shadow: var(--shadow);
|
||||||
|
--shadow-md: var(--shadow-md);
|
||||||
|
--shadow-lg: var(--shadow-lg);
|
||||||
|
--shadow-xl: var(--shadow-xl);
|
||||||
|
--shadow-2xl: var(--shadow-2xl);
|
||||||
|
|
||||||
|
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||||
|
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||||
|
--tracking-normal: var(--tracking-normal);
|
||||||
|
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||||
|
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||||
|
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
@layer base {
|
||||||
|
* {
|
||||||
|
@apply border-border outline-ring/50;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
@apply bg-background text-foreground;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
letter-spacing: var(--tracking-normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/app/layout 2.tsx
Normal file
26
src/app/layout 2.tsx
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import type { Metadata } from "next";
|
||||||
|
import "./globals.css";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "QuitTrack - Track Your Journey to Quit Smoking",
|
||||||
|
description: "Track and manage your smoking habits, set goals, and quit safely with personalized plans.",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
}: Readonly<{
|
||||||
|
children: React.ReactNode;
|
||||||
|
}>) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Space+Mono:wght@400;700&display=swap" rel="stylesheet" />
|
||||||
|
</head>
|
||||||
|
<body className="antialiased">
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
13
src/app/page 2.tsx
Normal file
13
src/app/page 2.tsx
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { redirect } from 'next/navigation';
|
||||||
|
import { getUser } from '@/lib/session';
|
||||||
|
import { Dashboard } from '@/components/Dashboard';
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const user = await getUser();
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
redirect('/login');
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Dashboard user={user} />;
|
||||||
|
}
|
||||||
7
src/app/signout/page.tsx
Normal file
7
src/app/signout/page.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { signOut } from "@workos-inc/authkit-nextjs";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default async function SignOutPage() {
|
||||||
|
await signOut();
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
244
src/components/DailyInspirationCard.tsx
Normal file
244
src/components/DailyInspirationCard.tsx
Normal file
@ -0,0 +1,244 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useState, useMemo } from 'react';
|
||||||
|
import { Sparkles, Settings, BookOpen, Quote as QuoteIcon, Check } from 'lucide-react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
|
import { savePreferencesAsync, getPreferences, UserPreferences } from '@/lib/storage';
|
||||||
|
|
||||||
|
type Religion = 'christian' | 'muslim' | 'jewish' | 'secular';
|
||||||
|
|
||||||
|
interface Verse {
|
||||||
|
text: string;
|
||||||
|
source: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const VERSES: Record<Religion, Verse[]> = {
|
||||||
|
christian: [
|
||||||
|
{ text: "I can do all things through Christ which strengtheneth me.", source: "Philippians 4:13 (KJV)" },
|
||||||
|
{ text: "For God hath not given us the spirit of fear; but of power, and of love, and of a sound mind.", source: "2 Timothy 1:7 (KJV)" },
|
||||||
|
{ text: "But they that wait upon the LORD shall renew their strength; they shall mount up with wings as eagles.", source: "Isaiah 40:31 (KJV)" },
|
||||||
|
{ text: "Be strong and of a good courage; be not afraid, neither be thou dismayed: for the LORD thy God is with thee whithersoever thou goest.", source: "Joshua 1:9 (KJV)" },
|
||||||
|
{ text: "The LORD is my shepherd; I shall not want.", source: "Psalm 23:1 (KJV)" },
|
||||||
|
{ text: "Trust in the LORD with all thine heart; and lean not unto thine own understanding.", source: "Proverbs 3:5 (KJV)" },
|
||||||
|
{ text: "And be not conformed to this world: but be ye transformed by the renewing of your mind.", source: "Romans 12:2 (KJV)" },
|
||||||
|
{ text: "Cast thy burden upon the LORD, and he shall sustain thee.", source: "Psalm 55:22 (KJV)" },
|
||||||
|
{ text: "Fear thou not; for I am with thee: be not dismayed; for I am thy God: I will strengthen thee.", source: "Isaiah 41:10 (KJV)" },
|
||||||
|
{ text: "Come unto me, all ye that labour and are heavy laden, and I will give you rest.", source: "Matthew 11:28 (KJV)" },
|
||||||
|
{ text: "The name of the LORD is a strong tower: the righteous runneth into it, and is safe.", source: "Proverbs 18:10 (KJV)" },
|
||||||
|
{ text: "Peace I leave with you, my peace I give unto you: not as the world giveth, give I unto you.", source: "John 14:27 (KJV)" },
|
||||||
|
{ text: "If God be for us, who can be against us?", source: "Romans 8:31 (KJV)" },
|
||||||
|
{ text: "He healeth the broken in heart, and bindeth up their wounds.", source: "Psalm 147:3 (KJV)" },
|
||||||
|
{ text: "Wait on the LORD: be of good courage, and he shall strengthen thine heart.", source: "Psalm 27:14 (KJV)" },
|
||||||
|
{ text: "My flesh and my heart faileth: but God is the strength of my heart, and my portion for ever.", source: "Psalm 73:26 (KJV)" },
|
||||||
|
{ text: "God is our refuge and strength, a very present help in trouble.", source: "Psalm 46:1 (KJV)" },
|
||||||
|
{ text: "In him was life; and the life was the light of men.", source: "John 1:4 (KJV)" },
|
||||||
|
{ 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" },
|
||||||
|
{ text: "The greatest glory in living lies not in never falling, but in rising every time we fall.", source: "Nelson Mandela" },
|
||||||
|
{ text: "In the middle of difficulty lies opportunity.", source: "Albert Einstein" },
|
||||||
|
{ text: "What you get by achieving your goals is not as important as what you become by achieving your goals.", source: "Zig Ziglar" },
|
||||||
|
{ text: "The future belongs to those who believe in the beauty of their dreams.", source: "Eleanor Roosevelt" },
|
||||||
|
{ text: "It does not matter how slowly you go as long as you do not stop.", source: "Confucius" },
|
||||||
|
{ text: "Everything you've ever wanted is on the other side of fear.", source: "George Addair" },
|
||||||
|
{ text: "The best time to plant a tree was 20 years ago. The second best time is now.", source: "Chinese Proverb" },
|
||||||
|
{ text: "You are never too old to set another goal or to dream a new dream.", source: "C.S. Lewis" },
|
||||||
|
{ text: "The only impossible journey is the one you never begin.", source: "Tony Robbins" },
|
||||||
|
{ text: "Success is not final, failure is not fatal: it is the courage to continue that counts.", source: "Winston Churchill" },
|
||||||
|
{ text: "Believe you can and you're halfway there.", source: "Theodore Roosevelt" },
|
||||||
|
{ text: "The pain you feel today will be the strength you feel tomorrow.", source: "Arnold Schwarzenegger" },
|
||||||
|
{ text: "Your life does not get better by chance, it gets better by change.", source: "Jim Rohn" },
|
||||||
|
{ text: "The secret of change is to focus all of your energy not on fighting the old, but on building the new.", source: "Socrates" },
|
||||||
|
{ text: "What lies behind us and what lies before us are tiny matters compared to what lies within us.", source: "Ralph Waldo Emerson" },
|
||||||
|
{ text: "The man who moves a mountain begins by carrying away small stones.", source: "Confucius" },
|
||||||
|
{ text: "Our greatest weakness lies in giving up. The most certain way to succeed is always to try just one more time.", source: "Thomas Edison" },
|
||||||
|
{ text: "Fall seven times, stand up eight.", source: "Japanese Proverb" },
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DailyInspirationCardProps {
|
||||||
|
initialReligion?: Religion | null;
|
||||||
|
onReligionChange?: (religion: Religion) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function DailyInspirationCard({ initialReligion, onReligionChange }: DailyInspirationCardProps) {
|
||||||
|
const [currentReligion, setCurrentReligion] = useState<Religion>(initialReligion || 'secular');
|
||||||
|
|
||||||
|
// Get a quote based on the day of the year and selected religion
|
||||||
|
const dailyVerse = useMemo(() => {
|
||||||
|
const verses = VERSES[currentReligion];
|
||||||
|
const now = new Date();
|
||||||
|
const start = new Date(now.getFullYear(), 0, 0);
|
||||||
|
const diff = now.getTime() - start.getTime();
|
||||||
|
const oneDay = 1000 * 60 * 60 * 24;
|
||||||
|
const dayOfYear = Math.floor(diff / oneDay);
|
||||||
|
return verses[dayOfYear % verses.length];
|
||||||
|
}, [currentReligion]);
|
||||||
|
|
||||||
|
const handleReligionChange = async (religion: Religion) => {
|
||||||
|
setCurrentReligion(religion);
|
||||||
|
|
||||||
|
// Save to backend if we can
|
||||||
|
const prefs = getPreferences();
|
||||||
|
if (prefs) {
|
||||||
|
const updatedPrefs: UserPreferences = {
|
||||||
|
...prefs,
|
||||||
|
religion: religion
|
||||||
|
};
|
||||||
|
await savePreferencesAsync(updatedPrefs);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onReligionChange) {
|
||||||
|
onReligionChange(religion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getLabel = (r: Religion) => {
|
||||||
|
switch (r) {
|
||||||
|
case 'christian': return 'Christian (Bible)';
|
||||||
|
case 'muslim': return 'Muslim (Quran)';
|
||||||
|
case 'jewish': return 'Jewish (Torah)';
|
||||||
|
case 'secular': return 'Secular (Quotes)';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getIcon = () => {
|
||||||
|
switch (currentReligion) {
|
||||||
|
case 'christian':
|
||||||
|
case 'muslim':
|
||||||
|
case 'jewish':
|
||||||
|
return <BookOpen className="h-4 w-4 text-yellow-300 animate-pulse-subtle" />;
|
||||||
|
default:
|
||||||
|
return <Sparkles className="h-4 w-4 text-yellow-300 animate-pulse-subtle" />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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)';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="flex-1 flex flex-col justify-center p-5 rounded-xl border border-white/10 min-h-[120px] relative overflow-hidden group"
|
||||||
|
style={{
|
||||||
|
background: getBackground(),
|
||||||
|
boxShadow: `inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 4px 20px ${getShadowColor()}`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-white/10 to-transparent rounded-full -translate-y-1/2 translate-x-1/2" />
|
||||||
|
|
||||||
|
<div className="relative z-10">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getIcon()}
|
||||||
|
<span className="text-xs font-semibold text-white/80 uppercase tracking-wider">
|
||||||
|
{currentReligion === 'secular' ? 'Daily Inspiration' : 'Daily Verse'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger asChild>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="icon"
|
||||||
|
className="h-6 w-6 text-white/50 hover:text-white hover:bg-white/10 rounded-full"
|
||||||
|
>
|
||||||
|
<Settings className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end">
|
||||||
|
{(['christian', 'muslim', 'jewish', 'secular'] as Religion[]).map((r) => (
|
||||||
|
<DropdownMenuItem
|
||||||
|
key={r}
|
||||||
|
onClick={() => handleReligionChange(r)}
|
||||||
|
className="flex items-center justify-between gap-2"
|
||||||
|
>
|
||||||
|
<span>{getLabel(r)}</span>
|
||||||
|
{currentReligion === r && <Check className="h-4 w-4" />}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
))}
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="text-sm font-medium text-white leading-relaxed mb-3 text-shadow-sm italic">
|
||||||
|
“{dailyVerse.text}”
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-white/70 font-medium">
|
||||||
|
— {dailyVerse.source}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -116,7 +116,7 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
init();
|
init();
|
||||||
}, [loadData, checkAndUnlockAchievements]);
|
}, [loadData, checkAndUnlockAchievements]);
|
||||||
|
|
||||||
const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number }) => {
|
const handleSetupComplete = async (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => {
|
||||||
const today = new Date().toISOString().split('T')[0];
|
const today = new Date().toISOString().split('T')[0];
|
||||||
const newPrefs: UserPreferences = {
|
const newPrefs: UserPreferences = {
|
||||||
substance: data.substance,
|
substance: data.substance,
|
||||||
@ -126,6 +126,7 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
quitPlan: null,
|
quitPlan: null,
|
||||||
userName: data.name,
|
userName: data.name,
|
||||||
userAge: data.age,
|
userAge: data.age,
|
||||||
|
religion: data.religion,
|
||||||
};
|
};
|
||||||
await savePreferencesAsync(newPrefs);
|
await savePreferencesAsync(newPrefs);
|
||||||
setPreferences(newPrefs);
|
setPreferences(newPrefs);
|
||||||
@ -216,6 +217,12 @@ export function Dashboard({ user }: DashboardProps) {
|
|||||||
usageData={usageData}
|
usageData={usageData}
|
||||||
onDataUpdate={loadData}
|
onDataUpdate={loadData}
|
||||||
userId={user.id}
|
userId={user.id}
|
||||||
|
religion={preferences.religion}
|
||||||
|
onReligionUpdate={async (religion) => {
|
||||||
|
const updatedPrefs = { ...preferences, religion };
|
||||||
|
setPreferences(updatedPrefs);
|
||||||
|
await savePreferencesAsync(updatedPrefs);
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="opacity-0 animate-fade-in-up delay-200">
|
<div className="opacity-0 animate-fade-in-up delay-200">
|
||||||
|
|||||||
@ -21,7 +21,7 @@ import {
|
|||||||
|
|
||||||
interface SetupWizardProps {
|
interface SetupWizardProps {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onComplete: (data: { substance: 'nicotine' | 'weed'; name: string; age: number }) => void;
|
onComplete: (data: { substance: 'nicotine' | 'weed'; name: string; age: number; religion: 'christian' | 'muslim' | 'jewish' | 'secular' }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
||||||
@ -29,6 +29,7 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
|||||||
const [name, setName] = useState('');
|
const [name, setName] = useState('');
|
||||||
const [age, setAge] = useState('25');
|
const [age, setAge] = useState('25');
|
||||||
const [substance, setSubstance] = useState<'nicotine' | 'weed' | ''>('');
|
const [substance, setSubstance] = useState<'nicotine' | 'weed' | ''>('');
|
||||||
|
const [religion, setReligion] = useState<'christian' | 'muslim' | 'jewish' | 'secular' | ''>('');
|
||||||
|
|
||||||
const ages = Array.from({ length: 83 }, (_, i) => (i + 18).toString());
|
const ages = Array.from({ length: 83 }, (_, i) => (i + 18).toString());
|
||||||
|
|
||||||
@ -37,15 +38,18 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
|||||||
setStep(2);
|
setStep(2);
|
||||||
} else if (step === 2 && age) {
|
} else if (step === 2 && age) {
|
||||||
setStep(3);
|
setStep(3);
|
||||||
|
} else if (step === 3 && substance) {
|
||||||
|
setStep(4);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleComplete = () => {
|
const handleComplete = () => {
|
||||||
if (substance && name.trim() && age) {
|
if (substance && name.trim() && age && religion) {
|
||||||
onComplete({
|
onComplete({
|
||||||
substance,
|
substance,
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
age: parseInt(age, 10),
|
age: parseInt(age, 10),
|
||||||
|
religion: religion,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -59,6 +63,7 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
|||||||
{step === 1 && "Let's get to know you a little better."}
|
{step === 1 && "Let's get to know you a little better."}
|
||||||
{step === 2 && "Just one more thing about you."}
|
{step === 2 && "Just one more thing about you."}
|
||||||
{step === 3 && "Set up your tracking preferences."}
|
{step === 3 && "Set up your tracking preferences."}
|
||||||
|
{step === 4 && "What inspires you?"}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
@ -143,7 +148,38 @@ export function SetupWizard({ open, onComplete }: SetupWizardProps) {
|
|||||||
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
<Button variant="outline" onClick={() => setStep(2)} className="flex-1">
|
||||||
Back
|
Back
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={handleComplete} disabled={!substance} className="flex-1">
|
<Button onClick={handleNext} disabled={!substance} className="flex-1">
|
||||||
|
Continue
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{step === 4 && (
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-3">
|
||||||
|
<Label htmlFor="religion">Choose your daily inspiration</Label>
|
||||||
|
<Select value={religion} onValueChange={(v) => setReligion(v as 'christian' | 'muslim' | 'jewish' | 'secular')}>
|
||||||
|
<SelectTrigger id="religion">
|
||||||
|
<SelectValue placeholder="Select source" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="christian">Christian (Bible Verses)</SelectItem>
|
||||||
|
<SelectItem value="muslim">Muslim (Quran Verses)</SelectItem>
|
||||||
|
<SelectItem value="jewish">Jewish (Torah/Tanakh)</SelectItem>
|
||||||
|
<SelectItem value="secular">Secular (Motivational Quotes)</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
We'll show you a new verse or quote each day to help keep you motivated.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Button variant="outline" onClick={() => setStep(3)} className="flex-1">
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleComplete} disabled={!religion} className="flex-1">
|
||||||
Start Tracking
|
Start Tracking
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -15,69 +15,25 @@ import { Label } from '@/components/ui/label';
|
|||||||
import { UsageEntry, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
|
import { UsageEntry, setUsageForDateAsync, clearDayDataAsync } from '@/lib/storage';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
|
import { ChevronLeftIcon, ChevronRightIcon, Cigarette, Leaf, Sparkles } from 'lucide-react';
|
||||||
import { useTheme } from '@/lib/theme-context';
|
import { useTheme } from '@/lib/theme-context';
|
||||||
|
import { DailyInspirationCard } from './DailyInspirationCard';
|
||||||
|
|
||||||
|
|
||||||
const quotes = [
|
|
||||||
{ text: "The only way to do great work is to love what you do.", author: "Steve Jobs" },
|
|
||||||
{ text: "It is during our darkest moments that we must focus to see the light.", author: "Aristotle" },
|
|
||||||
{ text: "The greatest glory in living lies not in never falling, but in rising every time we fall.", author: "Nelson Mandela" },
|
|
||||||
{ text: "In the middle of difficulty lies opportunity.", author: "Albert Einstein" },
|
|
||||||
{ text: "What you get by achieving your goals is not as important as what you become by achieving your goals.", author: "Zig Ziglar" },
|
|
||||||
{ text: "The future belongs to those who believe in the beauty of their dreams.", author: "Eleanor Roosevelt" },
|
|
||||||
{ text: "It does not matter how slowly you go as long as you do not stop.", author: "Confucius" },
|
|
||||||
{ text: "Everything you've ever wanted is on the other side of fear.", author: "George Addair" },
|
|
||||||
{ text: "The best time to plant a tree was 20 years ago. The second best time is now.", author: "Chinese Proverb" },
|
|
||||||
{ text: "You are never too old to set another goal or to dream a new dream.", author: "C.S. Lewis" },
|
|
||||||
{ text: "The only impossible journey is the one you never begin.", author: "Tony Robbins" },
|
|
||||||
{ text: "Success is not final, failure is not fatal: it is the courage to continue that counts.", author: "Winston Churchill" },
|
|
||||||
{ text: "Believe you can and you're halfway there.", author: "Theodore Roosevelt" },
|
|
||||||
{ text: "The pain you feel today will be the strength you feel tomorrow.", author: "Arnold Schwarzenegger" },
|
|
||||||
{ text: "Your life does not get better by chance, it gets better by change.", author: "Jim Rohn" },
|
|
||||||
{ text: "The secret of change is to focus all of your energy not on fighting the old, but on building the new.", author: "Socrates" },
|
|
||||||
{ text: "What lies behind us and what lies before us are tiny matters compared to what lies within us.", author: "Ralph Waldo Emerson" },
|
|
||||||
{ text: "The man who moves a mountain begins by carrying away small stones.", author: "Confucius" },
|
|
||||||
{ text: "Our greatest weakness lies in giving up. The most certain way to succeed is always to try just one more time.", author: "Thomas Edison" },
|
|
||||||
{ text: "Fall seven times, stand up eight.", author: "Japanese Proverb" },
|
|
||||||
{ text: "You don't have to be great to start, but you have to start to be great.", author: "Zig Ziglar" },
|
|
||||||
{ text: "The only person you are destined to become is the person you decide to be.", author: "Ralph Waldo Emerson" },
|
|
||||||
{ text: "When you reach the end of your rope, tie a knot in it and hang on.", author: "Franklin D. Roosevelt" },
|
|
||||||
{ text: "Do not wait to strike till the iron is hot; but make it hot by striking.", author: "William Butler Yeats" },
|
|
||||||
{ text: "Whether you think you can or you think you can't, you're right.", author: "Henry Ford" },
|
|
||||||
{ text: "The mind is everything. What you think you become.", author: "Buddha" },
|
|
||||||
{ text: "Strength does not come from physical capacity. It comes from an indomitable will.", author: "Mahatma Gandhi" },
|
|
||||||
{ text: "A journey of a thousand miles begins with a single step.", author: "Lao Tzu" },
|
|
||||||
{ text: "He who conquers himself is the mightiest warrior.", author: "Confucius" },
|
|
||||||
{ text: "The wound is the place where the light enters you.", author: "Rumi" },
|
|
||||||
{ text: "Rock bottom became the solid foundation on which I rebuilt my life.", author: "J.K. Rowling" },
|
|
||||||
{ text: "You have power over your mind, not outside events. Realize this, and you will find strength.", author: "Marcus Aurelius" },
|
|
||||||
{ text: "Out of suffering have emerged the strongest souls.", author: "Kahlil Gibran" },
|
|
||||||
{ text: "The only limit to our realization of tomorrow will be our doubts of today.", author: "Franklin D. Roosevelt" },
|
|
||||||
{ text: "Courage is not the absence of fear, but rather the judgment that something else is more important than fear.", author: "Ambrose Redmoon" },
|
|
||||||
{ text: "Every morning brings new potential, but if you dwell on the misfortunes of the day before, you tend to overlook tremendous opportunities.", author: "Harvey Mackay" },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface UsageCalendarProps {
|
interface UsageCalendarProps {
|
||||||
usageData: UsageEntry[];
|
usageData: UsageEntry[];
|
||||||
onDataUpdate: () => void;
|
onDataUpdate: () => void;
|
||||||
userId: string;
|
userId: string;
|
||||||
|
religion?: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
|
||||||
|
onReligionUpdate?: (religion: 'christian' | 'muslim' | 'jewish' | 'secular') => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
export function UsageCalendar({ usageData, onDataUpdate, religion, onReligionUpdate }: UsageCalendarProps) {
|
||||||
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
const [selectedDate, setSelectedDate] = useState<Date | undefined>(undefined);
|
||||||
const [editNicotineCount, setEditNicotineCount] = useState('');
|
const [editNicotineCount, setEditNicotineCount] = useState('');
|
||||||
const [editWeedCount, setEditWeedCount] = useState('');
|
const [editWeedCount, setEditWeedCount] = useState('');
|
||||||
const [isEditing, setIsEditing] = useState(false);
|
const [isEditing, setIsEditing] = useState(false);
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
|
|
||||||
// Get a quote based on the day of the year
|
|
||||||
const dailyQuote = useMemo(() => {
|
|
||||||
const now = new Date();
|
|
||||||
const start = new Date(now.getFullYear(), 0, 0);
|
|
||||||
const diff = now.getTime() - start.getTime();
|
|
||||||
const oneDay = 1000 * 60 * 60 * 24;
|
|
||||||
const dayOfYear = Math.floor(diff / oneDay);
|
|
||||||
return quotes[dayOfYear % quotes.length];
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getUsageForDate = (date: Date, substance: 'nicotine' | 'weed'): number => {
|
const getUsageForDate = (date: Date, substance: 'nicotine' | 'weed'): number => {
|
||||||
const dateStr = date.toISOString().split('T')[0];
|
const dateStr = date.toISOString().split('T')[0];
|
||||||
const entry = usageData.find((e) => e.date === dateStr && e.substance === substance);
|
const entry = usageData.find((e) => e.date === dateStr && e.substance === substance);
|
||||||
@ -205,9 +161,8 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
|||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
style={!isFuture ? colorStyle : undefined}
|
style={!isFuture ? colorStyle : undefined}
|
||||||
className={`relative w-full h-full p-2 text-sm rounded-md transition-all hover:opacity-80 ${
|
className={`relative w-full h-full p-2 text-sm rounded-md transition-all hover:opacity-80 ${isFuture ? 'text-muted-foreground opacity-30 cursor-not-allowed' : 'cursor-pointer shadow-sm'
|
||||||
isFuture ? 'text-muted-foreground opacity-30 cursor-not-allowed' : 'cursor-pointer shadow-sm'
|
} ${isToday ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-background' : ''}`}
|
||||||
} ${isToday ? 'ring-2 ring-amber-400 ring-offset-2 ring-offset-background' : ''}`}
|
|
||||||
onClick={() => !isFuture && handleDateSelect(date)}
|
onClick={() => !isFuture && handleDateSelect(date)}
|
||||||
disabled={isFuture}
|
disabled={isFuture}
|
||||||
>
|
>
|
||||||
@ -261,28 +216,11 @@ export function UsageCalendar({ usageData, onDataUpdate }: UsageCalendarProps) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Daily Quote */}
|
{/* Daily Inspiration */}
|
||||||
<div
|
<DailyInspirationCard
|
||||||
className="flex-1 flex flex-col justify-center p-5 rounded-xl border border-indigo-500/40 min-h-[120px] relative overflow-hidden"
|
initialReligion={religion}
|
||||||
style={{
|
onReligionChange={onReligionUpdate}
|
||||||
background: 'linear-gradient(135deg, rgba(67, 56, 202, 0.35) 0%, rgba(109, 40, 217, 0.3) 50%, rgba(76, 29, 149, 0.4) 100%)',
|
/>
|
||||||
boxShadow: 'inset 0 1px 0 rgba(255, 255, 255, 0.1), 0 4px 20px rgba(99, 102, 241, 0.15)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div className="absolute top-0 right-0 w-32 h-32 bg-gradient-to-br from-purple-500/20 to-transparent rounded-full -translate-y-1/2 translate-x-1/2" />
|
|
||||||
<div className="relative z-10">
|
|
||||||
<div className="flex items-center gap-2 mb-3">
|
|
||||||
<Sparkles className="h-4 w-4 text-yellow-300 animate-pulse-subtle" />
|
|
||||||
<span className="text-xs font-semibold text-white/80 uppercase tracking-wider">Daily Inspiration</span>
|
|
||||||
</div>
|
|
||||||
<p className="text-sm font-medium text-white leading-relaxed mb-3 text-shadow-sm">
|
|
||||||
“{dailyQuote.text}”
|
|
||||||
</p>
|
|
||||||
<p className="text-xs text-white/70 font-medium">
|
|
||||||
— {dailyQuote.author}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 grid grid-cols-2 sm:grid-cols-3 md:flex md:flex-wrap gap-3 text-sm">
|
<div className="mt-4 grid grid-cols-2 sm:grid-cols-3 md:flex md:flex-wrap gap-3 text-sm">
|
||||||
|
|||||||
147
src/hooks/use-storage.ts
Normal file
147
src/hooks/use-storage.ts
Normal file
@ -0,0 +1,147 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { useState, useEffect, useCallback } from "react";
|
||||||
|
import {
|
||||||
|
getUserData,
|
||||||
|
saveUserData,
|
||||||
|
getUsageLogs,
|
||||||
|
addUsageLog,
|
||||||
|
getRecentLogs,
|
||||||
|
calculateStats,
|
||||||
|
getQuitPlan,
|
||||||
|
generateQuitPlan,
|
||||||
|
getCurrentWeek,
|
||||||
|
canGeneratePlan,
|
||||||
|
needsCheckIn as checkNeedsCheckIn,
|
||||||
|
} from "@/lib/storage";
|
||||||
|
import { UserData, UsageLog, QuitPlan, SubstanceType } from "@/types";
|
||||||
|
|
||||||
|
export function useUserData() {
|
||||||
|
const [userData, setUserData] = useState<UserData | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setUserData(getUserData());
|
||||||
|
setIsLoading(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const updateUserData = useCallback((data: Partial<UserData>) => {
|
||||||
|
const updated = saveUserData(data);
|
||||||
|
setUserData(updated);
|
||||||
|
return updated;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const completeOnboarding = useCallback(
|
||||||
|
(substanceType: SubstanceType, stayLoggedIn: boolean) => {
|
||||||
|
return updateUserData({
|
||||||
|
substanceType,
|
||||||
|
stayLoggedIn,
|
||||||
|
onboardingComplete: true,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[updateUserData]
|
||||||
|
);
|
||||||
|
|
||||||
|
return {
|
||||||
|
userData,
|
||||||
|
isLoading,
|
||||||
|
updateUserData,
|
||||||
|
completeOnboarding,
|
||||||
|
isOnboardingComplete: userData?.onboardingComplete ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUsageLogs() {
|
||||||
|
const [logs, setLogs] = useState<UsageLog[]>([]);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const refreshLogs = useCallback(() => {
|
||||||
|
setLogs(getUsageLogs());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshLogs();
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [refreshLogs]);
|
||||||
|
|
||||||
|
const logUsage = useCallback(
|
||||||
|
(puffs: number, date?: string) => {
|
||||||
|
const newLog = addUsageLog(puffs, date);
|
||||||
|
refreshLogs();
|
||||||
|
return newLog;
|
||||||
|
},
|
||||||
|
[refreshLogs]
|
||||||
|
);
|
||||||
|
|
||||||
|
const getRecent = useCallback((days: number = 7) => {
|
||||||
|
return getRecentLogs(days);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const stats = calculateStats(getRecentLogs(7));
|
||||||
|
|
||||||
|
return {
|
||||||
|
logs,
|
||||||
|
isLoading,
|
||||||
|
logUsage,
|
||||||
|
getRecent,
|
||||||
|
refreshLogs,
|
||||||
|
stats,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useQuitPlan() {
|
||||||
|
const [plan, setPlan] = useState<QuitPlan | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const refreshPlan = useCallback(() => {
|
||||||
|
setPlan(getQuitPlan());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refreshPlan();
|
||||||
|
setIsLoading(false);
|
||||||
|
}, [refreshPlan]);
|
||||||
|
|
||||||
|
const createPlan = useCallback(() => {
|
||||||
|
const recentLogs = getRecentLogs(7);
|
||||||
|
const { averagePuffs } = calculateStats(recentLogs);
|
||||||
|
|
||||||
|
if (averagePuffs === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPlan = generateQuitPlan(averagePuffs);
|
||||||
|
setPlan(newPlan);
|
||||||
|
return newPlan;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const currentWeek = plan ? getCurrentWeek(plan) : 0;
|
||||||
|
const { canGenerate, daysTracked } = canGeneratePlan();
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan,
|
||||||
|
isLoading,
|
||||||
|
createPlan,
|
||||||
|
refreshPlan,
|
||||||
|
currentWeek,
|
||||||
|
canGenerate,
|
||||||
|
daysTracked,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCheckIn() {
|
||||||
|
const [needsCheckIn, setNeedsCheckIn] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setNeedsCheckIn(checkNeedsCheckIn());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const markCheckedIn = useCallback(() => {
|
||||||
|
setNeedsCheckIn(false);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return {
|
||||||
|
needsCheckIn,
|
||||||
|
markCheckedIn,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ export interface UserPreferences {
|
|||||||
quitPlan: QuitPlan | null;
|
quitPlan: QuitPlan | null;
|
||||||
userName: string | null;
|
userName: string | null;
|
||||||
userAge: number | null;
|
userAge: number | null;
|
||||||
|
religion: 'christian' | 'muslim' | 'jewish' | 'secular' | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuitPlan {
|
export interface QuitPlan {
|
||||||
@ -95,6 +96,7 @@ const defaultPreferences: UserPreferences = {
|
|||||||
quitPlan: null,
|
quitPlan: null,
|
||||||
userName: null,
|
userName: null,
|
||||||
userAge: null,
|
userAge: null,
|
||||||
|
religion: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Cache for preferences and usage data to avoid excessive API calls
|
// Cache for preferences and usage data to avoid excessive API calls
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user