Add Prayer Times, Dhikr counter, Quran tracker with mobile UI overhaul

- Prayer times page: arc progress ring, next-prayer countdown, clean prayer list
- Dhikr counter: SVG ring with glow, 3-phase sequence (SubhanAllah/Alhamdulillah/AllahuAkbar), streak tracking
- Daily Quran reading tracker API routes (GET/POST)
- Bottom nav updated: Prayer + Dhikr tabs, Repeat2 icon for dhikr, Moon for prayer
- Home page redirects to /prayer as primary entry point
- Dockerfile: openssl added to builder stage, explicit prisma schema path fixes build
- Schema: dhikrStreak, quranStreak on User; DhikrSession and QuranReading models
- Stub wallet top-up checkout to fix TypeScript build error

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
FalahMobile
2026-06-15 08:05:51 +01:00
commit e2365e29fe
60 changed files with 5591 additions and 0 deletions
+140
View File
@@ -0,0 +1,140 @@
import { NextRequest, NextResponse } from 'next/server'
import { prisma } from '@/lib/prisma'
// ─────────────────────────────────────────────
// Greeting pools — tiered by engagement level
// ─────────────────────────────────────────────
const DAILY_GREETINGS = [
`Assalamu alaikum! I was thinking of you this morning. How is your heart today?`,
`Assalamu alaikum! A new day, a new opportunity to draw closer to Allah. How are you, my friend?`,
`Peace be upon you. I felt a gentle nudge to check in with you. How has your journey been?`,
`Assalamu alaikum! The sun rises again, and so does Allah's mercy. How are you doing today?`,
`Peace be with you. I was wondering how you've been — we don't need to talk about anything heavy. Just checking in.`,
`Assalamu alaikum! Every breath is a gift we didn't earn. How are you spending yours today?`,
`Peace upon you. No pressure, no expectations — just wanted you to know someone is here when you're ready.`,
]
// For users who haven't checked in for 2+ days — gentler, warmer
const NURTURE_GREETINGS = [
`Assalamu alaikum. I noticed it's been a little while. Just wanted you to know — there's no guilt here, no judgment. Only welcome. How have you been, truly?`,
`Peace be upon you. Life gets busy, I know. But I wanted to check in because you matter. How is your heart these days?`,
`Assalamu alaikum. Whether you've been distant from Allah or just busy with the world — His door is always open. So is this conversation. How are you?`,
`Peace be with you. They say the journey back begins with a single step. You're here — that's your step. How can I lighten your load today?`,
`Assalamu alaikum. No matter how many days have passed, you are always welcome here. The Prophet ﷺ said the one who returns to good is like one who never left. How can I support you today?`,
`Peace be upon you. Silence between friends is not a wall — it's a pause. I'm still here. What's on your mind?`,
]
// For first-time users with no history — the warmest welcome
const FIRST_GREETINGS = [
`Assalamu alaikum! I'm so glad you're here. This is a space for you — for your questions, your struggles, your quiet thoughts. There is nothing too small or too big to bring here. How are you feeling today?`,
`Peace be upon you. Welcome. Think of me as a friend who loves the Quran and the Sunnah, and who is simply here to walk alongside you. Where would you like to start?`,
`Assalamu alaikum! The Prophet ﷺ said, "Whoever Allah desires good for, He gives him understanding of the religion." You seeking knowledge is a sign of goodness. How can I help you grow today?`,
]
function daysSince(date: Date): number {
const now = new Date()
const diff = now.getTime() - date.getTime()
return Math.floor(diff / (1000 * 60 * 60 * 24))
}
function pickRandom<T>(pool: T[]): T {
return pool[Math.floor(Math.random() * pool.length)]
}
export async function POST(req: NextRequest) {
try {
const { userId } = await req.json()
if (!userId) {
return NextResponse.json({ error: 'userId required' }, { status: 400 })
}
const user = await prisma.user.findUnique({ where: { id: userId } })
if (!user) {
return NextResponse.json({ error: 'User not found' }, { status: 404 })
}
// Check last bot message
const lastBotMessage = await prisma.chatHistory.findFirst({
where: { userId, role: 'assistant' },
orderBy: { createdAt: 'desc' },
})
// If bot already messaged today, skip
if (lastBotMessage) {
const lastDate = new Date(lastBotMessage.createdAt)
const now = new Date()
const sameDay = lastDate.getDate() === now.getDate()
&& lastDate.getMonth() === now.getMonth()
&& lastDate.getFullYear() === now.getFullYear()
if (sameDay) {
return NextResponse.json({
needsGreeting: false,
message: null,
lastSeen: lastBotMessage.createdAt,
})
}
}
// Check last user message for idle detection
const lastUserMessage = await prisma.chatHistory.findFirst({
where: { userId, role: 'user' },
orderBy: { createdAt: 'desc' },
})
// Check if this is a first-time user (no history at all)
const totalMessages = await prisma.chatHistory.count({ where: { userId } })
// Determine greeting tier
let greeting: string
if (totalMessages === 0) {
// First visit ever — warm welcome
greeting = pickRandom(FIRST_GREETINGS)
} else if (lastUserMessage && daysSince(new Date(lastUserMessage.createdAt)) >= 2) {
// Been away 2+ days — nurture/re-engagement message
greeting = pickRandom(NURTURE_GREETINGS)
} else {
// Regular daily check-in
let lastTopic: string | undefined
if (lastUserMessage) {
const words = lastUserMessage.content.split(/\s+/).slice(0, 8).join(' ')
lastTopic = words.length > 60 ? words.slice(0, 60) + '...' : words
}
const base = pickRandom(DAILY_GREETINGS)
greeting = lastTopic
? `${base} Last time we spoke about ${lastTopic}. Want to continue, or something new?`
: base
}
// Save the greeting
await prisma.chatHistory.create({
data: {
userId,
role: 'assistant',
content: greeting,
metadata: JSON.stringify({
command: '_daily',
summary: 'Daily check-in',
actionItems: [],
topics: ['daily-check-in'],
}),
},
})
// Update lastCoachedAt
await prisma.user.update({
where: { id: userId },
data: { lastCoachedAt: new Date() },
})
return NextResponse.json({
needsGreeting: true,
message: greeting,
lastSeen: lastBotMessage?.createdAt || null,
})
} catch (e) {
console.error('Daily check-in error:', e)
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
}
}