Add Docker support and containerization
- Add Dockerfile for Next.js application - Configure standalone output for Docker - Add .dockerignore file - Add docker-compose.yml for easy deployment - Update next.config.ts for Docker compatibility
This commit is contained in:
1 parent
91d417159d
commit
fba1097380
20 files changed
+6085
-253
No files matched your search
+588
-56
@@ -1,65 +1,597 @@
|
||||
import Image from "next/image";
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { useAuth } from '@/context/AuthContext';
|
||||
import Login from '@/components/Login';
|
||||
import Admin from '@/components/Admin';
|
||||
|
||||
interface Prayer {
|
||||
id: string;
|
||||
date: string;
|
||||
type: 'prayer' | 'praise';
|
||||
text: string;
|
||||
journal: string;
|
||||
email: string;
|
||||
prayFor: string;
|
||||
archived?: boolean;
|
||||
reminderFrequency?: 'daily' | 'weekly' | 'monthly' | 'never';
|
||||
includeActiveSummary?: boolean;
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<div className="flex min-h-screen items-center justify-center bg-zinc-50 font-sans dark:bg-black">
|
||||
<main className="flex min-h-screen w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/next.svg"
|
||||
alt="Next.js logo"
|
||||
width={100}
|
||||
height={20}
|
||||
priority
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
|
||||
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
|
||||
To get started, edit the page.tsx file.
|
||||
const [prayers, setPrayers] = useState<Prayer[]>([]);
|
||||
const [form, setForm] = useState({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
type: 'prayer' as 'prayer' | 'praise',
|
||||
text: '',
|
||||
journal: '',
|
||||
email: '',
|
||||
prayFor: '',
|
||||
reminderFrequency: 'never' as 'daily' | 'weekly' | 'monthly' | 'never',
|
||||
includeActiveSummary: false,
|
||||
});
|
||||
const [showAdmin, setShowAdmin] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [activeTab, setActiveTab] = useState<'add' | 'active' | 'archived' | 'search'>('add');
|
||||
const itemsPerPage = 10;
|
||||
const { user, loading, logout, isAdmin } = useAuth();
|
||||
const [showLoadingScreen, setShowLoadingScreen] = useState(true);
|
||||
|
||||
// Ensure loading screen shows for at least 5 seconds
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => {
|
||||
setShowLoadingScreen(false);
|
||||
}, 5000); // 5 seconds minimum
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('prayers');
|
||||
if (stored) {
|
||||
setPrayers(JSON.parse(stored));
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem('prayers', JSON.stringify(prayers));
|
||||
}, [prayers]);
|
||||
|
||||
// Auto-populate email field with user's email
|
||||
useEffect(() => {
|
||||
if (user?.email) {
|
||||
setForm(prev => ({ ...prev, email: user.email! }));
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
const newPrayer: Prayer = {
|
||||
id: Date.now().toString(),
|
||||
...form,
|
||||
};
|
||||
setPrayers(prev => [...prev, newPrayer]);
|
||||
|
||||
if (form.email && form.reminderFrequency !== 'never') {
|
||||
try {
|
||||
let message = `Date: ${form.date}\n\n${form.text}\n\nJournal: ${form.journal || 'None'}`;
|
||||
|
||||
if (form.prayFor) {
|
||||
message += `\n\nPraying for: ${form.prayFor}`;
|
||||
}
|
||||
|
||||
if (form.includeActiveSummary) {
|
||||
const activePrayers = prayers.filter(prayer => !prayer.archived);
|
||||
if (activePrayers.length > 0) {
|
||||
message += `\n\n--- Active Prayers Summary ---\n`;
|
||||
activePrayers.forEach(prayer => {
|
||||
message += `\n${prayer.date} - ${prayer.type.toUpperCase()}: ${prayer.text}`;
|
||||
if (prayer.prayFor) {
|
||||
message += ` (Praying for: ${prayer.prayFor})`;
|
||||
}
|
||||
if (prayer.journal) {
|
||||
message += `\n Journal: ${prayer.journal}`;
|
||||
}
|
||||
message += `\n`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
await fetch('/api/send-email', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
email: form.email,
|
||||
subject: `New ${form.type} added - ${form.reminderFrequency} reminders enabled`,
|
||||
message: message,
|
||||
}),
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to send email:', error);
|
||||
}
|
||||
}
|
||||
|
||||
setForm({
|
||||
date: new Date().toISOString().split('T')[0],
|
||||
type: 'prayer',
|
||||
text: '',
|
||||
journal: '',
|
||||
email: user?.email || '',
|
||||
prayFor: '',
|
||||
reminderFrequency: 'never',
|
||||
includeActiveSummary: false,
|
||||
});
|
||||
};
|
||||
|
||||
const activePrayers = prayers.filter(prayer => !prayer.archived);
|
||||
const archivedPrayers = prayers.filter(prayer => prayer.archived);
|
||||
|
||||
// Filter prayers based on search term
|
||||
const filteredActivePrayers = activePrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
const filteredArchivedPrayers = archivedPrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// For search tab, combine all filtered prayers
|
||||
const allPrayers = [...activePrayers, ...archivedPrayers];
|
||||
const filteredSearchPrayers = allPrayers.filter(prayer =>
|
||||
prayer.text.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.journal.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
prayer.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
|
||||
// Pagination logic
|
||||
const totalActivePages = Math.ceil(filteredActivePrayers.length / itemsPerPage);
|
||||
const totalArchivedPages = Math.ceil(filteredArchivedPrayers.length / itemsPerPage);
|
||||
const totalSearchPages = Math.ceil(filteredSearchPrayers.length / itemsPerPage);
|
||||
|
||||
const paginatedActivePrayers = filteredActivePrayers.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const paginatedArchivedPrayers = filteredArchivedPrayers.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const paginatedSearchPrayers = filteredSearchPrayers.slice(
|
||||
(currentPage - 1) * itemsPerPage,
|
||||
currentPage * itemsPerPage
|
||||
);
|
||||
|
||||
const currentPrayers = activeTab === 'active' ? paginatedActivePrayers :
|
||||
activeTab === 'archived' ? paginatedArchivedPrayers :
|
||||
paginatedSearchPrayers;
|
||||
const currentTotalPages = activeTab === 'active' ? totalActivePages :
|
||||
activeTab === 'archived' ? totalArchivedPages :
|
||||
totalSearchPages;
|
||||
|
||||
const groupedPrayers = currentPrayers.reduce((acc, prayer) => {
|
||||
if (!acc[prayer.date]) acc[prayer.date] = [];
|
||||
acc[prayer.date].push(prayer);
|
||||
return acc;
|
||||
}, {} as Record<string, Prayer[]>);
|
||||
|
||||
const sharePrayer = async (prayer: Prayer) => {
|
||||
const shareText = `${prayer.type.toUpperCase()}: ${prayer.text}${prayer.journal ? `\n\nJournal: ${prayer.journal}` : ''}`;
|
||||
|
||||
if (navigator.share) {
|
||||
try {
|
||||
await navigator.share({
|
||||
title: `Shared ${prayer.type}`,
|
||||
text: shareText,
|
||||
});
|
||||
// Show success feedback
|
||||
const button = document.activeElement as HTMLElement;
|
||||
if (button) {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '✅ Shared!';
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
}, 2000);
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Share cancelled');
|
||||
}
|
||||
} else {
|
||||
// Fallback: copy to clipboard
|
||||
try {
|
||||
await navigator.clipboard.writeText(shareText);
|
||||
// Show success feedback
|
||||
const button = document.activeElement as HTMLElement;
|
||||
if (button) {
|
||||
const originalText = button.textContent;
|
||||
button.textContent = '📋 Copied!';
|
||||
button.classList.add('bg-green-600', 'hover:bg-green-700');
|
||||
setTimeout(() => {
|
||||
button.textContent = originalText;
|
||||
button.classList.remove('bg-green-600', 'hover:bg-green-700');
|
||||
}, 2000);
|
||||
}
|
||||
} catch (clipboardErr) {
|
||||
alert('Prayer copied to clipboard!');
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (loading || showLoadingScreen) {
|
||||
return (
|
||||
<div className="min-h-screen bg-gradient-to-br from-gray-900 via-black to-gray-800 flex items-center justify-center p-4">
|
||||
<div className="text-center">
|
||||
{/* Animated prayer icon */}
|
||||
<div className="mb-8 relative">
|
||||
<div className="w-24 h-24 mx-auto relative">
|
||||
{/* Outer ring */}
|
||||
<div className="absolute inset-0 border-4 border-blue-500/30 rounded-full animate-spin"></div>
|
||||
{/* Inner ring */}
|
||||
<div className="absolute inset-2 border-4 border-transparent border-t-blue-400 rounded-full animate-spin" style={{animationDirection: 'reverse', animationDuration: '1.5s'}}></div>
|
||||
{/* Center icon */}
|
||||
<div className="absolute inset-0 flex items-center justify-center">
|
||||
<div className="text-3xl animate-pulse">🙏</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Floating particles */}
|
||||
<div className="absolute -top-2 -right-2 w-3 h-3 bg-blue-400 rounded-full animate-bounce" style={{animationDelay: '0s'}}></div>
|
||||
<div className="absolute -bottom-1 -left-3 w-2 h-2 bg-purple-400 rounded-full animate-bounce" style={{animationDelay: '0.5s'}}></div>
|
||||
<div className="absolute top-1/2 -right-4 w-2 h-2 bg-green-400 rounded-full animate-bounce" style={{animationDelay: '1s'}}></div>
|
||||
</div>
|
||||
|
||||
{/* Loading text */}
|
||||
<h1 className="text-2xl font-bold text-white mb-2 animate-pulse">
|
||||
Prayer App
|
||||
</h1>
|
||||
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
|
||||
Looking for a starting point or more instructions? Head over to{" "}
|
||||
<a
|
||||
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Templates
|
||||
</a>{" "}
|
||||
or the{" "}
|
||||
<a
|
||||
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
className="font-medium text-zinc-950 dark:text-zinc-50"
|
||||
>
|
||||
Learning
|
||||
</a>{" "}
|
||||
center.
|
||||
<p className="text-gray-400 text-lg mb-4">
|
||||
Preparing your spiritual journey...
|
||||
</p>
|
||||
|
||||
{/* Inspirational quote */}
|
||||
<div className="max-w-xs mx-auto">
|
||||
<blockquote className="text-gray-300 italic text-sm leading-relaxed">
|
||||
"Be anxious for nothing, but in everything by prayer and supplication, with thanksgiving, let your requests be made known to God."
|
||||
</blockquote>
|
||||
<cite className="text-gray-500 text-xs mt-2 block">
|
||||
— Philippians 4:6
|
||||
</cite>
|
||||
</div>
|
||||
|
||||
{/* Progress dots */}
|
||||
<div className="flex justify-center space-x-2 mt-8">
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{animationDelay: '0s'}}></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{animationDelay: '0.2s'}}></div>
|
||||
<div className="w-2 h-2 bg-blue-500 rounded-full animate-pulse" style={{animationDelay: '0.4s'}}></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return <Login />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-black text-white p-4">
|
||||
<div className="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Prayer App</h1>
|
||||
<p className="text-gray-400 text-sm mt-1">
|
||||
{activeTab === 'add' ? 'Add New Prayer or Praise' :
|
||||
activeTab === 'search' ? 'Search Results' :
|
||||
`${activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Prayers`}
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
|
||||
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Image
|
||||
className="dark:invert"
|
||||
src="/vercel.svg"
|
||||
alt="Vercel logomark"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
Deploy Now
|
||||
</a>
|
||||
<a
|
||||
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
|
||||
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Documentation
|
||||
</a>
|
||||
<div className="flex gap-4">
|
||||
{isAdmin && (
|
||||
<button
|
||||
onClick={() => setShowAdmin(!showAdmin)}
|
||||
className="px-4 py-3 bg-purple-600 hover:bg-purple-700 rounded text-base font-medium"
|
||||
>
|
||||
{showAdmin ? 'Hide Admin' : 'Admin'}
|
||||
</button>
|
||||
)}
|
||||
<button onClick={() => logout()} className="px-4 py-3 bg-red-600 hover:bg-red-700 rounded text-base font-medium">
|
||||
Logout
|
||||
</button>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{showAdmin && <Admin />}
|
||||
|
||||
{activeTab === 'add' && (
|
||||
<form onSubmit={handleSubmit} className="max-w-md mx-auto mb-8 space-y-6">
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Date</label>
|
||||
<input
|
||||
type="date"
|
||||
value={form.date}
|
||||
onChange={e => setForm(prev => ({ ...prev, date: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Type</label>
|
||||
<select
|
||||
value={form.type}
|
||||
onChange={e => setForm(prev => ({ ...prev, type: e.target.value as 'prayer' | 'praise' }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base"
|
||||
>
|
||||
<option value="prayer">Prayer</option>
|
||||
<option value="praise">Praise</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Pray For (Person)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={form.prayFor}
|
||||
onChange={e => setForm(prev => ({ ...prev, prayFor: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base"
|
||||
placeholder="Name of person to pray for..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Text</label>
|
||||
<textarea
|
||||
value={form.text}
|
||||
onChange={e => setForm(prev => ({ ...prev, text: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base min-h-[100px]"
|
||||
placeholder="Enter your prayer or praise..."
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Journal</label>
|
||||
<textarea
|
||||
value={form.journal}
|
||||
onChange={e => setForm(prev => ({ ...prev, journal: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base min-h-[100px]"
|
||||
placeholder="Optional journal entry..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Email (for reminders)</label>
|
||||
<input
|
||||
type="email"
|
||||
value={form.email}
|
||||
onChange={e => setForm(prev => ({ ...prev, email: e.target.value }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block mb-2 text-lg">Reminder Frequency</label>
|
||||
<select
|
||||
value={form.reminderFrequency}
|
||||
onChange={e => setForm(prev => ({ ...prev, reminderFrequency: e.target.value as 'daily' | 'weekly' | 'monthly' | 'never' }))}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded text-base"
|
||||
>
|
||||
<option value="never">Never</option>
|
||||
<option value="daily">Daily</option>
|
||||
<option value="weekly">Weekly</option>
|
||||
<option value="monthly">Monthly</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center mb-4">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={form.includeActiveSummary}
|
||||
onChange={e => setForm(prev => ({ ...prev, includeActiveSummary: e.target.checked }))}
|
||||
className="w-6 h-6 text-blue-600 border-gray-300 rounded focus:ring-blue-500"
|
||||
/>
|
||||
<label className="ml-3 text-base font-medium" onClick={() => setForm(prev => ({ ...prev, includeActiveSummary: !prev.includeActiveSummary }))}>
|
||||
Include active prayers summary in reminders
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" className="w-full p-4 bg-blue-600 hover:bg-blue-700 rounded text-lg font-semibold mt-6">
|
||||
Add Entry
|
||||
</button>
|
||||
</form>
|
||||
)}
|
||||
|
||||
{activeTab !== 'add' && (
|
||||
<div className="max-w-4xl mx-auto">
|
||||
{activeTab === 'search' && (
|
||||
<div className="mb-6">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search all prayers..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
setCurrentPage(1); // Reset to first page when searching
|
||||
}}
|
||||
className="w-full p-3 bg-gray-800 border border-gray-600 rounded-lg text-white placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
autoFocus
|
||||
/>
|
||||
{searchTerm && (
|
||||
<p className="text-gray-400 text-sm mt-2">
|
||||
Found {filteredSearchPrayers.length} prayer{filteredSearchPrayers.length !== 1 ? 's' : ''}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentPrayers.length === 0 && (
|
||||
<div className="text-center py-8">
|
||||
<p className="text-gray-500">
|
||||
{activeTab === 'search' && searchTerm ? 'No prayers found matching your search.' : 'No prayers or praises found.'}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.keys(groupedPrayers).sort().reverse().map(date => (
|
||||
<div key={date} className="mb-8">
|
||||
<h2 className="text-xl font-semibold mb-4">{new Date(date).toLocaleDateString()}</h2>
|
||||
{groupedPrayers[date].map(prayer => (
|
||||
<div key={prayer.id} className={`bg-gray-800 p-4 rounded mb-4 ${prayer.archived || activeTab === 'search' ? 'opacity-75 border border-gray-600' : ''}`}>
|
||||
<div className="flex justify-between items-start mb-3">
|
||||
<span className="font-medium capitalize text-lg">{prayer.type}</span>
|
||||
<button
|
||||
onClick={() => sharePrayer(prayer)}
|
||||
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium rounded-lg flex items-center gap-2 transition-all duration-200 shadow-md hover:shadow-lg"
|
||||
title="Share this prayer"
|
||||
>
|
||||
📤 Share
|
||||
</button>
|
||||
</div>
|
||||
<p className="mb-2">{prayer.text}</p>
|
||||
{prayer.journal && (
|
||||
<div className="border-t border-gray-600 pt-2">
|
||||
<p className="text-sm text-gray-400">Journal: {prayer.journal}</p>
|
||||
</div>
|
||||
)}
|
||||
{prayer.prayFor && (
|
||||
<div className="border-t border-gray-600 pt-2">
|
||||
<p className="text-sm text-gray-400">Pray For: {prayer.prayFor}</p>
|
||||
</div>
|
||||
)}
|
||||
{activeTab !== 'search' && (
|
||||
<div className="flex justify-end mt-3 pt-2 border-t border-gray-600">
|
||||
{prayer.archived ? (
|
||||
<button
|
||||
onClick={() => {
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayer.id ? { ...p, archived: false } : p
|
||||
));
|
||||
}}
|
||||
className="px-3 py-1 text-green-500 hover:text-green-400 text-sm font-medium hover:bg-gray-700 rounded transition-colors"
|
||||
title="Restore prayer"
|
||||
>
|
||||
↩️ Restore
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
onClick={() => {
|
||||
setPrayers(prev => prev.map(p =>
|
||||
p.id === prayer.id ? { ...p, archived: true } : p
|
||||
));
|
||||
}}
|
||||
className="px-3 py-1 text-red-500 hover:text-red-400 text-sm font-medium hover:bg-gray-700 rounded transition-colors"
|
||||
title="Archive prayer"
|
||||
>
|
||||
📂 Archive
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Pagination */}
|
||||
{currentTotalPages > 1 && (
|
||||
<div className="flex justify-center items-center gap-4 mt-8 mb-6">
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.max(1, prev - 1))}
|
||||
disabled={currentPage === 1}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded text-white"
|
||||
>
|
||||
Previous
|
||||
</button>
|
||||
|
||||
<span className="text-gray-300">
|
||||
Page {currentPage} of {currentTotalPages}
|
||||
</span>
|
||||
|
||||
<button
|
||||
onClick={() => setCurrentPage(prev => Math.min(currentTotalPages, prev + 1))}
|
||||
disabled={currentPage === currentTotalPages}
|
||||
className="px-4 py-2 bg-gray-700 hover:bg-gray-600 disabled:bg-gray-800 disabled:cursor-not-allowed rounded text-white"
|
||||
>
|
||||
Next
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Bottom Navigation Tabs */}
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-gradient-to-t from-gray-900 via-gray-900 to-gray-800 border-t border-gray-700 shadow-2xl">
|
||||
<div className="grid grid-cols-4 px-2 py-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('add');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`mx-1 py-3 px-2 rounded-xl text-center transition-all duration-200 text-sm font-semibold flex flex-col items-center justify-center min-h-[60px] ${
|
||||
activeTab === 'add'
|
||||
? 'bg-gradient-to-br from-blue-600 to-blue-700 text-white shadow-lg transform scale-105'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800/50 active:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mb-1">➕</span>
|
||||
<span className="text-xs leading-tight">Add</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('active');
|
||||
setCurrentPage(1);
|
||||
setSearchTerm(''); // Clear search when switching to active
|
||||
}}
|
||||
className={`mx-1 py-3 px-2 rounded-xl text-center transition-all duration-200 text-sm font-semibold flex flex-col items-center justify-center min-h-[60px] ${
|
||||
activeTab === 'active'
|
||||
? 'bg-gradient-to-br from-green-600 to-green-700 text-white shadow-lg transform scale-105'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800/50 active:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mb-1">📖</span>
|
||||
<span className="text-xs leading-tight">Active</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('archived');
|
||||
setCurrentPage(1);
|
||||
setSearchTerm(''); // Clear search when switching to archived
|
||||
}}
|
||||
className={`mx-1 py-3 px-2 rounded-xl text-center transition-all duration-200 text-sm font-semibold flex flex-col items-center justify-center min-h-[60px] ${
|
||||
activeTab === 'archived'
|
||||
? 'bg-gradient-to-br from-purple-600 to-purple-700 text-white shadow-lg transform scale-105'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800/50 active:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mb-1">📂</span>
|
||||
<span className="text-xs leading-tight">Archived</span>
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
setActiveTab('search');
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
className={`mx-1 py-3 px-2 rounded-xl text-center transition-all duration-200 text-sm font-semibold flex flex-col items-center justify-center min-h-[60px] ${
|
||||
activeTab === 'search'
|
||||
? 'bg-gradient-to-br from-orange-600 to-orange-700 text-white shadow-lg transform scale-105'
|
||||
: 'text-gray-400 hover:text-white hover:bg-gray-800/50 active:bg-gray-700/50'
|
||||
}`}
|
||||
>
|
||||
<span className="text-lg mb-1">🔍</span>
|
||||
<span className="text-xs leading-tight">Search</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Add padding to account for fixed bottom nav */}
|
||||
<div className="pb-20"></div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user