Add bottom navigation, History and Settings pages with improved UI styling

This commit is contained in:
Shreyansh Saboo
2026-02-19 14:19:19 +05:30
parent 555c03a91c
commit aec080ba54
8 changed files with 1731 additions and 28 deletions

View File

@@ -4,6 +4,9 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Lora:ital,wght@0,400;0,500;1,400&family=Playfair+Display:wght@400;500;600;700&display=swap" rel="stylesheet">
<title>Grateful Journal</title>
</head>
<body>

File diff suppressed because it is too large Load Diff

View File

@@ -2,6 +2,8 @@ import { BrowserRouter, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from './contexts/AuthContext'
import { ProtectedRoute } from './components/ProtectedRoute'
import HomePage from './pages/HomePage'
import HistoryPage from './pages/HistoryPage'
import SettingsPage from './pages/SettingsPage'
import LoginPage from './pages/LoginPage'
import './App.css'
@@ -18,6 +20,22 @@ function App() {
</ProtectedRoute>
}
/>
<Route
path="/history"
element={
<ProtectedRoute>
<HistoryPage />
</ProtectedRoute>
}
/>
<Route
path="/settings"
element={
<ProtectedRoute>
<SettingsPage />
</ProtectedRoute>
}
/>
<Route path="/login" element={<LoginPage />} />
<Route path="*" element={<Navigate to="/login" replace />} />
</Routes>

View File

@@ -0,0 +1,48 @@
import { useNavigate, useLocation } from 'react-router-dom'
export default function BottomNav() {
const navigate = useNavigate()
const location = useLocation()
const isActive = (path: string) => location.pathname === path
return (
<nav className="bottom-nav">
<button
type="button"
className={`bottom-nav-btn ${isActive('/') ? 'bottom-nav-btn-active' : ''}`}
onClick={() => navigate('/')}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 19l7-7 3 3-7 7-3-3z"></path>
<path d="M18 13l-1.5-7.5L2 2l3.5 14.5L13 18l5-5z"></path>
<path d="M2 2l7.586 7.586"></path>
<circle cx="11" cy="11" r="2"></circle>
</svg>
<span>Write</span>
</button>
<button
type="button"
className={`bottom-nav-btn ${isActive('/history') ? 'bottom-nav-btn-active' : ''}`}
onClick={() => navigate('/history')}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
<span>History</span>
</button>
<button
type="button"
className={`bottom-nav-btn ${isActive('/settings') ? 'bottom-nav-btn-active' : ''}`}
onClick={() => navigate('/settings')}
>
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="12" cy="12" r="3"></circle>
<path d="M12 1v6m0 6v6m5.196-15.804l-4.243 4.243m-5.657 5.657l-4.243 4.243M23 12h-6m-6 0H1m15.804 5.196l-4.243-4.243m-5.657-5.657L2.661 2.661"></path>
</svg>
<span>Settings</span>
</button>
</nav>
)
}

View File

@@ -1,4 +1,4 @@
/* Grateful Journal green palette from Coolors */
/* Grateful Journal enhanced green palette */
*,
*::before,
*::after {
@@ -6,7 +6,7 @@
}
:root {
font-family: system-ui, -apple-system, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
line-height: 1.5;
font-weight: 400;
/* Responsive base: 16px at 320px, scales up to 18px by 768px */
@@ -19,7 +19,7 @@
--color-accent-light: #cff2dc;
--color-accent-bright: #c3fd2f;
--color-text: #1a1a1a;
--color-text-muted: #555;
--color-text-muted: #6b7280;
--color-border: #cff2dc;
color: var(--color-text);

180
src/pages/HistoryPage.tsx Normal file
View File

@@ -0,0 +1,180 @@
import { useState } from 'react'
import BottomNav from '../components/BottomNav'
interface JournalEntry {
id: string
date: Date
title: string
content: string
}
export default function HistoryPage() {
const [currentMonth, setCurrentMonth] = useState(new Date())
// Mock data - replace with actual Firebase data later
const mockEntries: JournalEntry[] = [
{
id: '1',
date: new Date(2026, 1, 12),
title: 'Feeling much lighter today',
content: 'After the long conversation yesterday, I woke up with a sense of clarity I haven\'t felt in weeks...'
},
{
id: '2',
date: new Date(2026, 1, 5),
title: 'Morning thoughts',
content: 'The coffee smells amazing this morning. Simple pleasures like this remind me...'
}
]
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear()
const month = date.getMonth()
const firstDay = new Date(year, month, 1)
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
const startingDayOfWeek = firstDay.getDay()
return { daysInMonth, startingDayOfWeek }
}
const hasEntryOnDate = (day: number) => {
return mockEntries.some(entry => {
const entryDate = new Date(entry.date)
return entryDate.getDate() === day &&
entryDate.getMonth() === currentMonth.getMonth() &&
entryDate.getFullYear() === currentMonth.getFullYear()
})
}
const isToday = (day: number) => {
const today = new Date()
return day === today.getDate() &&
currentMonth.getMonth() === today.getMonth() &&
currentMonth.getFullYear() === today.getFullYear()
}
const formatDate = (date: Date) => {
return date.toLocaleDateString('en-US', {
weekday: 'short',
month: 'short',
day: '2-digit'
}).toUpperCase()
}
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', {
hour: '2-digit',
minute: '2-digit'
}).toUpperCase()
}
const { daysInMonth, startingDayOfWeek } = getDaysInMonth(currentMonth)
const monthName = currentMonth.toLocaleDateString('en-US', { month: 'long', year: 'numeric' })
const previousMonth = () => {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))
}
const nextMonth = () => {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))
}
return (
<div className="history-page">
<div className="history-bg-decoration">
<div className="bg-orb bg-orb-1"></div>
<div className="bg-orb bg-orb-2"></div>
<div className="bg-pattern"></div>
</div>
<header className="history-header">
<div className="history-header-text">
<h1>History</h1>
<p className="history-subtitle">Your past reflections</p>
</div>
<button type="button" className="history-search-btn" title="Search entries">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="11" cy="11" r="8"></circle>
<path d="m21 21-4.35-4.35"></path>
</svg>
</button>
</header>
<main className="history-container">
<div className="calendar-card">
<div className="calendar-header">
<h2 className="calendar-month">{monthName}</h2>
<div className="calendar-nav">
<button type="button" onClick={previousMonth} className="calendar-nav-btn" title="Previous month">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="15 18 9 12 15 6"></polyline>
</svg>
</button>
<button type="button" onClick={nextMonth} className="calendar-nav-btn" title="Next month">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
</div>
<div className="calendar-grid">
<div className="calendar-weekday">S</div>
<div className="calendar-weekday">M</div>
<div className="calendar-weekday">T</div>
<div className="calendar-weekday">W</div>
<div className="calendar-weekday">T</div>
<div className="calendar-weekday">F</div>
<div className="calendar-weekday">S</div>
{Array.from({ length: startingDayOfWeek }).map((_, i) => (
<div key={`empty-${i}`} className="calendar-day calendar-day-empty"></div>
))}
{Array.from({ length: daysInMonth }).map((_, i) => {
const day = i + 1
const hasEntry = hasEntryOnDate(day)
const isTodayDate = isToday(day)
return (
<button
key={day}
type="button"
className={`calendar-day ${hasEntry ? 'calendar-day-has-entry' : ''} ${isTodayDate ? 'calendar-day-today' : ''}`}
onClick={() => console.log('View entries for', day)}
>
{day}
</button>
)
})}
</div>
</div>
<section className="recent-entries">
<h3 className="recent-entries-title">RECENT ENTRIES</h3>
<div className="entries-list">
{mockEntries.map(entry => (
<button
key={entry.id}
type="button"
className="entry-card"
onClick={() => console.log('Open entry', entry.id)}
>
<div className="entry-header">
<span className="entry-date">{formatDate(entry.date)}</span>
<span className="entry-time">{formatTime(entry.date)}</span>
</div>
<h4 className="entry-title">{entry.title}</h4>
<p className="entry-preview">{entry.content}</p>
</button>
))}
</div>
</section>
</main>
<BottomNav />
</div>
)
}

View File

@@ -1,8 +1,12 @@
import { useAuth } from '../contexts/AuthContext'
import { Link } from 'react-router-dom'
import { useState } from 'react'
import BottomNav from '../components/BottomNav'
export default function HomePage() {
const { user, loading, signOut } = useAuth()
const [entry, setEntry] = useState('')
const [title, setTitle] = useState('')
if (loading) {
return (
@@ -24,11 +28,32 @@ export default function HomePage() {
)
}
const displayName =
user.displayName ?? user.email ?? 'there'
const displayName = user.displayName ?? user.email ?? 'there'
// Get current date formatted like "THURSDAY, OCT 24"
const today = new Date()
const dateString = today.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
}).toUpperCase()
const handleWrite = () => {
console.log('Saving entry:', { title, entry })
// TODO: Save to Firebase
setTitle('')
setEntry('')
}
return (
<div className="home-page">
<div className="home-bg-decoration">
<div className="bg-orb bg-orb-1"></div>
<div className="bg-orb bg-orb-2"></div>
<div className="bg-orb bg-orb-3"></div>
<div className="bg-pattern"></div>
</div>
<header className="home-header">
<h1>Grateful Journal</h1>
<div className="home-user">
@@ -38,10 +63,32 @@ export default function HomePage() {
</button>
</div>
</header>
<main className="home-main">
<p className="home-welcome">Hello, {displayName}.</p>
<p className="home-sub">Your writing space will go here.</p>
<main className="journal-container">
<div className="journal-card">
<div className="journal-date">{dateString}</div>
<h2 className="journal-prompt">What are you grateful for today?</h2>
<div className="journal-writing-area">
<input
type="text"
className="journal-title-input"
placeholder="Title your thoughts..."
value={title}
onChange={(e) => setTitle(e.target.value)}
/>
<textarea
className="journal-entry-textarea"
placeholder=""
value={entry}
onChange={(e) => setEntry(e.target.value)}
/>
</div>
</div>
</main>
<BottomNav />
</div>
)
}

172
src/pages/SettingsPage.tsx Normal file
View File

@@ -0,0 +1,172 @@
import { useState } from 'react'
import { useAuth } from '../contexts/AuthContext'
import BottomNav from '../components/BottomNav'
export default function SettingsPage() {
const { user, signOut } = useAuth()
const [passcodeEnabled, setPasscodeEnabled] = useState(false)
const [faceIdEnabled, setFaceIdEnabled] = useState(false)
const displayName = user?.displayName || 'User'
const photoURL = user?.photoURL || ''
const handleClearData = () => {
if (window.confirm('Are you sure you want to clear all local data? This action cannot be undone.')) {
// TODO: Implement clear local data
console.log('Clearing local data...')
}
}
return (
<div className="settings-page">
<div className="settings-bg-decoration">
<div className="bg-orb bg-orb-1"></div>
<div className="bg-orb bg-orb-2"></div>
<div className="bg-pattern"></div>
</div>
<header className="settings-header">
<div className="settings-header-text">
<h1>Settings</h1>
<p className="settings-subtitle">Manage your privacy and preferences.</p>
</div>
</header>
<main className="settings-container">
{/* Profile Section */}
<div className="settings-profile">
<div className="settings-avatar">
{photoURL ? (
<img src={photoURL} alt={displayName} className="settings-avatar-img" />
) : (
<div className="settings-avatar-placeholder">
{displayName.charAt(0).toUpperCase()}
</div>
)}
</div>
<div className="settings-profile-info">
<h2 className="settings-profile-name">{displayName}</h2>
<span className="settings-profile-badge">PRO MEMBER</span>
</div>
</div>
{/* Privacy & Security */}
<section className="settings-section">
<h3 className="settings-section-title">PRIVACY & SECURITY</h3>
<div className="settings-card">
<div className="settings-item">
<div className="settings-item-icon settings-item-icon-green">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect>
<path d="M7 11V7a5 5 0 0 1 10 0v4"></path>
</svg>
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Passcode Lock</h4>
<p className="settings-item-subtitle">Secure your entries</p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={passcodeEnabled}
onChange={(e) => setPasscodeEnabled(e.target.checked)}
/>
<span className="settings-toggle-slider"></span>
</label>
</div>
<div className="settings-divider"></div>
<div className="settings-item">
<div className="settings-item-icon settings-item-icon-gray">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2z"></path>
<path d="M12 6v6l4 2"></path>
</svg>
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Face ID</h4>
<p className="settings-item-subtitle">Unlock with glance</p>
</div>
<label className="settings-toggle">
<input
type="checkbox"
checked={faceIdEnabled}
onChange={(e) => setFaceIdEnabled(e.target.checked)}
/>
<span className="settings-toggle-slider"></span>
</label>
</div>
</div>
</section>
{/* Data & Look */}
<section className="settings-section">
<h3 className="settings-section-title">DATA & LOOK</h3>
<div className="settings-card">
<button type="button" className="settings-item settings-item-button">
<div className="settings-item-icon settings-item-icon-orange">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="7 10 12 15 17 10"></polyline>
<line x1="12" y1="15" x2="12" y2="3"></line>
</svg>
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Export Journal</h4>
<p className="settings-item-subtitle">PDF or Plain Text</p>
</div>
<svg className="settings-item-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
<div className="settings-divider"></div>
<button type="button" className="settings-item settings-item-button">
<div className="settings-item-icon settings-item-icon-blue">
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<circle cx="13.5" cy="6.5" r=".5"></circle>
<circle cx="17.5" cy="10.5" r=".5"></circle>
<circle cx="8.5" cy="7.5" r=".5"></circle>
<circle cx="6.5" cy="12.5" r=".5"></circle>
<path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10c.926 0 1.648-.746 1.648-1.688 0-.437-.18-.835-.437-1.125-.29-.289-.438-.652-.438-1.125a1.64 1.64 0 0 1 1.668-1.668h1.996c3.051 0 5.555-2.503 5.555-5.554C21.965 6.012 17.461 2 12 2z"></path>
</svg>
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Theme</h4>
<p className="settings-item-subtitle">Currently: Warm Beige</p>
</div>
<div className="settings-theme-colors">
<span className="settings-theme-dot settings-theme-dot-beige"></span>
<span className="settings-theme-dot settings-theme-dot-dark"></span>
</div>
<svg className="settings-item-arrow" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="9 18 15 12 9 6"></polyline>
</svg>
</button>
</div>
</section>
{/* Clear Data */}
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
<span>Clear Local Data</span>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
</button>
{/* Sign Out */}
<button type="button" className="settings-signout-btn" onClick={() => signOut()}>
Sign Out
</button>
{/* Version */}
<p className="settings-version">VERSION 1.0.2</p>
</main>
<BottomNav />
</div>
)
}