mongog setup

This commit is contained in:
2026-03-04 12:23:13 +05:30
parent bed32863da
commit a9eaa7599c
32 changed files with 2577 additions and 670 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -13,12 +13,12 @@ import {
signOut as firebaseSignOut,
type User,
} from 'firebase/auth'
import { auth, googleProvider, db } from '../lib/firebase'
import { doc, setDoc } from 'firebase/firestore'
import { COLLECTIONS } from '../lib/firestoreConfig'
import { auth, googleProvider } from '../lib/firebase'
import { registerUser, getUserByEmail } from '../lib/api'
type AuthContextValue = {
user: User | null
userId: string | null
loading: boolean
signInWithGoogle: () => Promise<void>
signOut: () => Promise<void>
@@ -28,21 +28,33 @@ const AuthContext = createContext<AuthContextValue | null>(null)
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(null)
const [userId, setUserId] = useState<string | null>(null)
const [loading, setLoading] = useState(true)
// Save user info to Firestore when they authenticate
async function saveUserToFirestore(authUser: User) {
// Register or fetch user from MongoDB
async function syncUserWithDatabase(authUser: User) {
try {
const userRef = doc(db, COLLECTIONS.USERS, authUser.uid)
await setDoc(userRef, {
id: authUser.uid,
email: authUser.email || '',
displayName: authUser.displayName || '',
photoURL: authUser.photoURL || '',
lastLoginAt: Date.now(),
}, { merge: true })
const token = await authUser.getIdToken()
const email = authUser.email!
// Try to get existing user
try {
const existingUser = await getUserByEmail(email, token)
setUserId(existingUser.id)
} catch (error) {
// User doesn't exist, register them
const newUser = await registerUser(
{
email,
displayName: authUser.displayName || undefined,
photoURL: authUser.photoURL || undefined,
},
token
)
setUserId(newUser.id)
}
} catch (error) {
console.error('Error saving user to Firestore:', error)
console.error('Error syncing user with database:', error)
}
}
@@ -50,7 +62,9 @@ export function AuthProvider({ children }: { children: ReactNode }) {
const unsubscribe = onAuthStateChanged(auth, async (u) => {
setUser(u)
if (u) {
await saveUserToFirestore(u)
await syncUserWithDatabase(u)
} else {
setUserId(null)
}
setLoading(false)
})
@@ -64,10 +78,12 @@ export function AuthProvider({ children }: { children: ReactNode }) {
async function signOut() {
await firebaseSignOut(auth)
setUserId(null)
}
const value: AuthContextValue = {
user,
userId,
loading,
signInWithGoogle,
signOut,

View File

@@ -2,91 +2,95 @@
*,
*::before,
*::after {
box-sizing: border-box;
box-sizing: border-box;
}
:root {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Helvetica Neue", sans-serif;
line-height: 1.5;
font-weight: 400;
/* Fixed 16px we're always rendering at phone scale */
font-size: 16px;
font-family:
-apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto",
"Helvetica Neue", sans-serif;
line-height: 1.5;
font-weight: 400;
/* Fixed 16px we're always rendering at phone scale */
font-size: 16px;
--touch-min: 44px;
--touch-min: 44px;
--color-primary: #22c55e;
--color-primary-hover: #16a34a;
--color-bg-soft: #f5f0e8;
--color-surface: #ffffff;
--color-accent-light: #dcfce7;
--color-text: #1a1a1a;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
--color-primary: #22c55e;
--color-primary-hover: #16a34a;
--color-bg-soft: #f5f0e8;
--color-surface: #ffffff;
--color-accent-light: #dcfce7;
--color-text: #1a1a1a;
--color-text-muted: #6b7280;
--color-border: #e5e7eb;
color: var(--color-text);
background-color: var(--color-bg-soft);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: var(--color-text);
background-color: var(--color-bg-soft);
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
a {
font-weight: 500;
color: var(--color-primary);
text-decoration: inherit;
font-weight: 500;
color: var(--color-primary);
text-decoration: inherit;
}
a:hover {
color: var(--color-primary-hover);
color: var(--color-primary-hover);
}
html {
height: 100%;
-webkit-text-size-adjust: 100%;
height: 100%;
-webkit-text-size-adjust: 100%;
}
body {
margin: 0;
height: 100%;
min-height: 100dvh;
overflow: hidden;
/* Desktop: show as phone on a desk surface */
background: #ccc8c0;
margin: 0;
height: 100%;
min-height: 100dvh;
overflow: hidden;
/* Desktop: show as phone on a desk surface */
background: #ccc8c0;
}
/* ── Phone shell on desktop ───────────────────────────── */
@media (min-width: 600px) {
body {
display: flex;
align-items: center;
justify-content: center;
background: #bbb7af;
}
body {
display: flex;
align-items: center;
justify-content: center;
background: #bbb7af;
}
#root {
width: 390px !important;
max-width: 390px !important;
height: 100dvh;
max-height: 100dvh;
border-radius: 0;
overflow: hidden;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.35), 0 4px 16px rgba(0, 0, 0, 0.2);
position: relative;
flex-shrink: 0;
}
#root {
width: 390px !important;
max-width: 390px !important;
height: 100dvh;
max-height: 100dvh;
border-radius: 0;
overflow: hidden;
box-shadow:
0 24px 80px rgba(0, 0, 0, 0.35),
0 4px 16px rgba(0, 0, 0, 0.2);
position: relative;
flex-shrink: 0;
}
}
h1 {
font-size: 1.75rem;
line-height: 1.2;
font-size: 1.75rem;
line-height: 1.2;
}
button {
font-family: inherit;
cursor: pointer;
font-family: inherit;
cursor: pointer;
}
button:focus,
button:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}

173
src/lib/api.ts Normal file
View File

@@ -0,0 +1,173 @@
/**
* API Service Layer
* Handles all communication with the backend API
*/
const API_BASE_URL = 'http://localhost:8001'
type ApiOptions = {
method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
body?: unknown
token?: string | null
}
async function apiCall<T>(
endpoint: string,
options: ApiOptions = {}
): Promise<T> {
const { method = 'GET', body, token } = options
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
const config: RequestInit = {
method,
headers,
credentials: 'include',
}
if (body) {
config.body = JSON.stringify(body)
}
const response = await fetch(`${API_BASE_URL}${endpoint}`, config)
if (!response.ok) {
const error = await response.json().catch(() => ({}))
throw new Error(error.detail || `API error: ${response.statusText}`)
}
return response.json() as Promise<T>
}
// ============================================
// USER ENDPOINTS
// ============================================
export async function registerUser(
userData: {
email: string
displayName?: string
photoURL?: string
},
token: string
) {
return apiCall('/api/users/register', {
method: 'POST',
body: userData,
token,
})
}
export async function getUserByEmail(email: string, token: string) {
return apiCall(`/api/users/by-email/${email}`, { token })
}
export async function updateUserProfile(
userId: string,
updates: { displayName?: string; photoURL?: string; theme?: string },
token: string
) {
return apiCall(`/api/users/update/${userId}`, {
method: 'PUT',
body: updates,
token,
})
}
// ============================================
// ENTRY ENDPOINTS
// ============================================
export interface JournalEntryCreate {
title: string
content: string
mood?: string
tags?: string[]
isPublic?: boolean
}
export interface JournalEntry extends JournalEntryCreate {
id: string
userId: string
createdAt: string
updatedAt: string
}
export async function createEntry(
userId: string,
entryData: JournalEntryCreate,
token: string
) {
return apiCall<{ id: string; message: string }>(
`/api/entries/${userId}`,
{
method: 'POST',
body: entryData,
token,
}
)
}
export async function getUserEntries(
userId: string,
token: string,
limit = 50,
skip = 0
) {
return apiCall<{ entries: JournalEntry[]; total: number }>(
`/api/entries/${userId}?limit=${limit}&skip=${skip}`,
{ token }
)
}
export async function getEntry(
userId: string,
entryId: string,
token: string
) {
return apiCall<JournalEntry>(`/api/entries/${userId}/${entryId}`, {
token,
})
}
export async function updateEntry(
userId: string,
entryId: string,
updates: Partial<JournalEntryCreate>,
token: string
) {
return apiCall(`/api/entries/${userId}/${entryId}`, {
method: 'PUT',
body: updates,
token,
})
}
export async function deleteEntry(
userId: string,
entryId: string,
token: string
) {
return apiCall(`/api/entries/${userId}/${entryId}`, {
method: 'DELETE',
token,
})
}
export async function getEntriesByDate(
userId: string,
startDate: string,
endDate: string,
token: string
) {
return apiCall<JournalEntry[]>(
`/api/entries/${userId}/date-range?startDate=${startDate}&endDate=${endDate}`,
{ token }
)
}

View File

@@ -1,11 +1,9 @@
import { initializeApp } from 'firebase/app'
import { getAuth, GoogleAuthProvider } from 'firebase/auth'
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore'
const firebaseConfig = {
apiKey: import.meta.env.VITE_FIREBASE_API_KEY,
authDomain: import.meta.env.VITE_FIREBASE_AUTH_DOMAIN,
databaseURL: import.meta.env.VITE_FIREBASE_DATABASE_URL,
projectId: import.meta.env.VITE_FIREBASE_PROJECT_ID,
storageBucket: import.meta.env.VITE_FIREBASE_STORAGE_BUCKET,
messagingSenderId: import.meta.env.VITE_FIREBASE_MESSAGING_SENDER_ID,
@@ -14,14 +12,6 @@ const firebaseConfig = {
const app = initializeApp(firebaseConfig)
// Auth initialization
// Google Auth initialization
export const auth = getAuth(app)
export const googleProvider = new GoogleAuthProvider()
// Firestore initialization
export const db = getFirestore(app)
// Enable Firestore emulator in development (uncomment when testing locally)
// if (import.meta.env.DEV) {
// connectFirestoreEmulator(db, 'localhost', 8080)
// }

View File

@@ -1,17 +1,33 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { getUserEntries, type JournalEntry } from '../lib/api'
import BottomNav from '../components/BottomNav'
interface JournalEntry {
id: string
date: Date
title: string
content: string
}
export default function HistoryPage() {
const { user, userId, loading } = useAuth()
const [currentMonth, setCurrentMonth] = useState(new Date())
const entries: JournalEntry[] = []
const [entries, setEntries] = useState<JournalEntry[]>([])
const [loadingEntries, setLoadingEntries] = useState(false)
// Fetch entries on mount and when userId changes
useEffect(() => {
if (!user || !userId) return
const fetchEntries = async () => {
setLoadingEntries(true)
try {
const token = await user.getIdToken()
const response = await getUserEntries(userId, token, 100, 0)
setEntries(response.entries)
} catch (error) {
console.error('Error fetching entries:', error)
} finally {
setLoadingEntries(false)
}
}
fetchEntries()
}, [user, userId])
const getDaysInMonth = (date: Date) => {
const year = date.getFullYear()
@@ -20,43 +36,50 @@ export default function HistoryPage() {
const lastDay = new Date(year, month + 1, 0)
const daysInMonth = lastDay.getDate()
const startingDayOfWeek = firstDay.getDay()
return { daysInMonth, startingDayOfWeek }
}
const hasEntryOnDate = (day: number) => {
return entries.some(entry => {
const entryDate = new Date(entry.date)
return entryDate.getDate() === day &&
return entries.some((entry) => {
const entryDate = new Date(entry.createdAt)
return (
entryDate.getDate() === day &&
entryDate.getMonth() === currentMonth.getMonth() &&
entryDate.getFullYear() === currentMonth.getFullYear()
)
})
}
const isToday = (day: number) => {
const today = new Date()
return day === today.getDate() &&
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'
const formatDate = (date: string) => {
return new Date(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'
const formatTime = (date: string) => {
return new Date(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 monthName = currentMonth.toLocaleDateString('en-US', {
month: 'long',
year: 'numeric',
})
const previousMonth = () => {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() - 1))
@@ -66,6 +89,24 @@ export default function HistoryPage() {
setCurrentMonth(new Date(currentMonth.getFullYear(), currentMonth.getMonth() + 1))
}
// Get entries for current month
const currentMonthEntries = entries.filter((entry) => {
const entryDate = new Date(entry.createdAt)
return (
entryDate.getMonth() === currentMonth.getMonth() &&
entryDate.getFullYear() === currentMonth.getFullYear()
)
})
if (loading) {
return (
<div className="history-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p style={{ color: '#9ca3af' }}>Loading</p>
<BottomNav />
</div>
)
}
return (
<div className="history-page">
<header className="history-header">
@@ -116,7 +157,7 @@ export default function HistoryPage() {
const day = i + 1
const hasEntry = hasEntryOnDate(day)
const isTodayDate = isToday(day)
return (
<button
key={day}
@@ -133,28 +174,36 @@ export default function HistoryPage() {
<section className="recent-entries">
<h3 className="recent-entries-title">RECENT ENTRIES</h3>
<div className="entries-list">
{entries.length === 0 ? (
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: 'Inter, sans-serif' }}>
No entries yet. Start writing!
</p>
) : entries.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>
{loadingEntries ? (
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: 'Inter, sans-serif' }}>
Loading entries
</p>
) : (
<div className="entries-list">
{currentMonthEntries.length === 0 ? (
<p style={{ color: '#9ca3af', fontSize: '0.875rem', textAlign: 'center', padding: '1.5rem 0', fontFamily: 'Inter, sans-serif' }}>
No entries for this month yet. Start writing!
</p>
) : (
currentMonthEntries.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.createdAt)}</span>
<span className="entry-time">{formatTime(entry.createdAt)}</span>
</div>
<h4 className="entry-title">{entry.title}</h4>
<p className="entry-preview">{entry.content}</p>
</button>
))
)}
</div>
)}
</section>
</main>

View File

@@ -1,12 +1,15 @@
import { useAuth } from '../contexts/AuthContext'
import { Link } from 'react-router-dom'
import { useState } from 'react'
import { createEntry } from '../lib/api'
import BottomNav from '../components/BottomNav'
export default function HomePage() {
const { user, loading, signOut } = useAuth()
const { user, userId, loading, signOut } = useAuth()
const [entry, setEntry] = useState('')
const [title, setTitle] = useState('')
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
if (loading) {
return (
@@ -32,10 +35,39 @@ export default function HomePage() {
.toLocaleDateString('en-US', { weekday: 'long', month: 'short', day: 'numeric' })
.toUpperCase()
const handleWrite = () => {
// TODO: Save to Firebase
setTitle('')
setEntry('')
const handleWrite = async () => {
if (!userId || !title.trim() || !entry.trim()) {
setMessage({ type: 'error', text: 'Please add a title and entry content' })
return
}
setSaving(true)
setMessage(null)
try {
const token = await user.getIdToken()
await createEntry(
userId,
{
title: title.trim(),
content: entry.trim(),
isPublic: false,
},
token
)
setMessage({ type: 'success', text: 'Entry saved successfully!' })
setTitle('')
setEntry('')
// Clear success message after 3 seconds
setTimeout(() => setMessage(null), 3000)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to save entry'
setMessage({ type: 'error', text: errorMessage })
} finally {
setSaving(false)
}
}
return (
@@ -53,14 +85,40 @@ export default function HomePage() {
placeholder="Title your thoughts..."
value={title}
onChange={(e) => setTitle(e.target.value)}
disabled={saving}
/>
<textarea
className="journal-entry-textarea"
placeholder=""
value={entry}
onChange={(e) => setEntry(e.target.value)}
disabled={saving}
/>
</div>
{message && (
<div style={{
padding: '0.75rem',
marginTop: '1rem',
borderRadius: '8px',
fontSize: '0.875rem',
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
color: message.type === 'success' ? '#15803d' : '#b91c1c',
textAlign: 'center',
}}>
{message.text}
</div>
)}
<div style={{ marginTop: '1.5rem', display: 'flex', gap: '0.75rem' }}>
<button
className="journal-write-btn"
onClick={handleWrite}
disabled={saving || !title.trim() || !entry.trim()}
>
{saving ? 'Saving...' : 'Save Entry'}
</button>
</div>
</div>
</main>

View File

@@ -1,15 +1,39 @@
import { useState } from 'react'
import { useState, useEffect } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { updateUserProfile } from '../lib/api'
import BottomNav from '../components/BottomNav'
export default function SettingsPage() {
const { user, signOut } = useAuth()
const { user, userId, signOut, loading } = useAuth()
const [passcodeEnabled, setPasscodeEnabled] = useState(false)
const [faceIdEnabled, setFaceIdEnabled] = useState(false)
const [theme, setTheme] = useState<'light' | 'dark'>('light')
const [saving, setSaving] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const displayName = user?.displayName || 'User'
const photoURL = user?.photoURL || ''
const handleThemeChange = async (newTheme: 'light' | 'dark') => {
if (!userId || !user) return
setSaving(true)
setMessage(null)
try {
const token = await user.getIdToken()
await updateUserProfile(userId, { theme: newTheme }, token)
setTheme(newTheme)
setMessage({ type: 'success', text: 'Theme updated successfully!' })
setTimeout(() => setMessage(null), 2000)
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Failed to update theme'
setMessage({ type: 'error', text: errorMessage })
} finally {
setSaving(false)
}
}
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
@@ -17,6 +41,23 @@ export default function SettingsPage() {
}
}
const handleSignOut = async () => {
try {
await signOut()
} catch (error) {
console.error('Error signing out:', error)
}
}
if (loading) {
return (
<div className="settings-page" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<p style={{ color: '#9ca3af' }}>Loading</p>
<BottomNav />
</div>
)
}
return (
<div className="settings-page">
<header className="settings-header">
@@ -121,7 +162,7 @@ export default function SettingsPage() {
<div className="settings-divider"></div>
<button type="button" className="settings-item settings-item-button">
<div className="settings-item">
<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>
@@ -133,19 +174,44 @@ export default function SettingsPage() {
</div>
<div className="settings-item-content">
<h4 className="settings-item-title">Theme</h4>
<p className="settings-item-subtitle">Currently: Warm Beige</p>
<p className="settings-item-subtitle">Currently: {theme === 'light' ? 'Warm Beige' : 'Dark'}</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>
<button
type="button"
onClick={() => handleThemeChange('light')}
className="settings-theme-dot settings-theme-dot-beige"
style={{ opacity: theme === 'light' ? 1 : 0.5 }}
title="Light theme"
disabled={saving}
></button>
<button
type="button"
onClick={() => handleThemeChange('dark')}
className="settings-theme-dot settings-theme-dot-dark"
style={{ opacity: theme === 'dark' ? 1 : 0.5 }}
title="Dark theme"
disabled={saving}
></button>
</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>
</div>
</section>
{message && (
<div style={{
padding: '0.75rem',
marginBottom: '1rem',
borderRadius: '8px',
fontSize: '0.875rem',
backgroundColor: message.type === 'success' ? '#f0fdf4' : '#fef2f2',
color: message.type === 'success' ? '#15803d' : '#b91c1c',
textAlign: 'center',
}}>
{message.text}
</div>
)}
{/* Clear Data */}
<button type="button" className="settings-clear-btn" onClick={handleClearData}>
<span>Clear Local Data</span>
@@ -156,7 +222,7 @@ export default function SettingsPage() {
</button>
{/* Sign Out */}
<button type="button" className="settings-signout-btn" onClick={() => signOut()}>
<button type="button" className="settings-signout-btn" onClick={handleSignOut}>
Sign Out
</button>