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:
nmemmert committed 2025-10-22 12:42:41 -04:00
1 parent 91d417159d
commit fba1097380
20 files changed
+6085 -253

No files matched your search

+309
View File
@@ -0,0 +1,309 @@
'use client';
import { useState, useEffect } from 'react';
import { doc, getDoc, setDoc } from 'firebase/firestore';
import { db } from '@/lib/firebase';
import { useAuth } from '@/context/AuthContext';
interface SMTPConfig {
host: string;
port: number;
secure: boolean;
user: string;
pass: string;
from: string;
}
interface AdminConfig {
adminEmails: string[];
}
export default function Admin() {
const { user, isAdmin } = useAuth();
const [config, setConfig] = useState<SMTPConfig>({
host: '',
port: 587,
secure: false,
user: '',
pass: '',
from: '',
});
const [adminConfig, setAdminConfig] = useState<AdminConfig>({
adminEmails: [],
});
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [message, setMessage] = useState('');
const [newAdminEmail, setNewAdminEmail] = useState('');
useEffect(() => {
loadConfig();
}, []);
const loadConfig = async () => {
try {
// Load SMTP config
const smtpDoc = await getDoc(doc(db, 'config', 'smtp'));
if (smtpDoc.exists()) {
setConfig(smtpDoc.data() as SMTPConfig);
}
// Load admin config
const adminDoc = await getDoc(doc(db, 'config', 'admin'));
if (adminDoc.exists()) {
setAdminConfig(adminDoc.data() as AdminConfig);
} else {
// Default admin config
setAdminConfig({ adminEmails: ['your-admin-email@example.com'] });
}
} catch (error) {
console.error('Error loading config:', error);
} finally {
setLoading(false);
}
};
const saveConfig = async () => {
setSaving(true);
setMessage('');
try {
await setDoc(doc(db, 'config', 'smtp'), config);
setMessage('SMTP configuration saved successfully!');
} catch (error) {
console.error('Error saving config:', error);
setMessage('Error saving configuration.');
} finally {
setSaving(false);
}
};
const saveAdminConfig = async () => {
setSaving(true);
setMessage('');
try {
await setDoc(doc(db, 'config', 'admin'), adminConfig);
setMessage('Admin configuration saved successfully!');
} catch (error) {
console.error('Error saving admin config:', error);
setMessage('Error saving admin configuration.');
} finally {
setSaving(false);
}
};
const addAdminEmail = async () => {
if (!newAdminEmail.trim()) return;
const updatedEmails = [...adminConfig.adminEmails, newAdminEmail.trim()];
const updatedConfig = { ...adminConfig, adminEmails: updatedEmails };
try {
await setDoc(doc(db, 'config', 'admin'), updatedConfig);
setAdminConfig(updatedConfig);
setNewAdminEmail('');
setMessage('Admin email added successfully!');
} catch (error) {
console.error('Error adding admin email:', error);
setMessage('Error adding admin email.');
}
};
const removeAdminEmail = async (email: string) => {
const updatedEmails = adminConfig.adminEmails.filter(e => e !== email);
const updatedConfig = { ...adminConfig, adminEmails: updatedEmails };
try {
await setDoc(doc(db, 'config', 'admin'), updatedConfig);
setAdminConfig(updatedConfig);
setMessage('Admin email removed successfully!');
} catch (error) {
console.error('Error removing admin email:', error);
setMessage('Error removing admin email.');
}
};
if (!isAdmin) {
return (
<div className="max-w-2xl mx-auto p-6 bg-red-900 border border-red-700 rounded-lg">
<h2 className="text-xl font-bold text-red-100 mb-2">Access Denied</h2>
<p className="text-red-200">You don't have permission to access the admin panel.</p>
</div>
);
}
if (loading) {
return <div className="p-4">Loading admin settings...</div>;
}
return (
<div className="max-w-2xl mx-auto p-6 bg-gray-800 rounded-lg">
<h2 className="text-2xl font-bold mb-6 text-white">Admin Settings</h2>
<div className="space-y-4">
<h3 className="text-xl font-semibold text-white mb-4">SMTP Configuration</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">SMTP Host</label>
<input
type="text"
value={config.host}
onChange={(e) => setConfig(prev => ({ ...prev, host: e.target.value }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="smtp.gmail.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Port</label>
<input
type="number"
value={config.port}
onChange={(e) => setConfig(prev => ({ ...prev, port: parseInt(e.target.value) }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Username</label>
<input
type="text"
value={config.user}
onChange={(e) => setConfig(prev => ({ ...prev, user: e.target.value }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="your-email@gmail.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Password</label>
<input
type="password"
value={config.pass}
onChange={(e) => setConfig(prev => ({ ...prev, pass: e.target.value }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="your-app-password"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">From Email</label>
<input
type="email"
value={config.from}
onChange={(e) => setConfig(prev => ({ ...prev, from: e.target.value }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="noreply@yourapp.com"
/>
</div>
<div className="flex items-center">
<input
type="checkbox"
id="secure"
checked={config.secure}
onChange={(e) => setConfig(prev => ({ ...prev, secure: e.target.checked }))}
className="mr-2"
/>
<label htmlFor="secure" className="text-sm font-medium text-gray-300">Use SSL/TLS</label>
</div>
</div>
<div className="pt-4">
<button
onClick={saveConfig}
disabled={saving}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded text-white font-medium"
>
{saving ? 'Saving...' : 'Save SMTP Settings'}
</button>
{message && (
<p className={`mt-2 text-sm ${message.includes('Error') ? 'text-red-400' : 'text-green-400'}`}>
{message}
</p>
)}
</div>
<h3 className="text-xl font-semibold text-white mb-4">Admin Configuration</h3>
<div className="grid grid-cols-1 gap-4">
<div>
<label className="block text-sm font-medium text-gray-300 mb-1">Admin Emails</label>
<input
type="text"
value={adminConfig.adminEmails.join(', ')}
onChange={(e) => setAdminConfig(prev => ({ ...prev, adminEmails: e.target.value.split(',').map(email => email.trim()) }))}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="admin1@example.com, admin2@example.com"
/>
</div>
</div>
<div className="flex gap-2">
<input
type="text"
value={newAdminEmail}
onChange={(e) => setNewAdminEmail(e.target.value)}
className="flex-1 p-2 bg-gray-700 border border-gray-600 rounded text-white"
placeholder="new-admin@example.com"
/>
<button
onClick={addAdminEmail}
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-white font-medium"
>
Add Email
</button>
</div>
<div className="pt-4">
<button
onClick={saveAdminConfig}
disabled={saving}
className="px-6 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-blue-400 rounded text-white font-medium"
>
{saving ? 'Saving...' : 'Save Admin Settings'}
</button>
</div>
<div className="border-t border-gray-600 pt-6 mt-6">
<h3 className="text-xl font-semibold text-white mb-4">Admin Email Management</h3>
<div className="mb-4">
<label className="block text-sm font-medium text-gray-300 mb-2">Current Admin Emails</label>
<div className="space-y-2">
{adminConfig.adminEmails.map((email, index) => (
<div key={index} className="flex items-center justify-between bg-gray-700 p-2 rounded">
<span className="text-gray-200">{email}</span>
<button
onClick={() => removeAdminEmail(email)}
className="text-red-400 hover:text-red-300"
>
Remove
</button>
</div>
))}
</div>
</div>
<div className="flex gap-2">
<input
type="email"
value={newAdminEmail}
onChange={(e) => setNewAdminEmail(e.target.value)}
placeholder="new-admin@example.com"
className="flex-1 p-2 bg-gray-700 border border-gray-600 rounded text-white"
/>
<button
onClick={addAdminEmail}
className="px-4 py-2 bg-green-600 hover:bg-green-700 rounded text-white"
>
Add Admin
</button>
</div>
</div>
</div>
</div>
);
}
+93
View File
@@ -0,0 +1,93 @@
'use client';
import { useState } from 'react';
import { useAuth } from '@/context/AuthContext';
export default function Login() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [confirmPassword, setConfirmPassword] = useState('');
const [isSignup, setIsSignup] = useState(false);
const [error, setError] = useState('');
const { login, signup } = useAuth();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
if (isSignup) {
if (password !== confirmPassword) {
setError('Passwords do not match');
return;
}
try {
await signup(email, password);
} catch (err: any) {
setError(err.message);
}
} else {
try {
await login(email, password);
} catch (err: any) {
setError(err.message);
}
}
};
return (
<div className="min-h-screen bg-black text-white flex items-center justify-center">
<form onSubmit={handleSubmit} className="bg-gray-800 p-8 rounded-lg w-full max-w-md">
<h1 className="text-2xl font-bold mb-6 text-center">
{isSignup ? 'Sign Up for Prayer App' : 'Login to Prayer App'}
</h1>
{error && <p className="text-red-500 mb-4">{error}</p>}
<div className="mb-4">
<label className="block mb-2">Email</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
required
/>
</div>
<div className="mb-4">
<label className="block mb-2">Password</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
required
/>
</div>
{isSignup && (
<div className="mb-6">
<label className="block mb-2">Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
className="w-full p-2 bg-gray-700 border border-gray-600 rounded"
required
/>
</div>
)}
<button type="submit" className="w-full p-2 bg-blue-600 hover:bg-blue-700 rounded mb-4">
{isSignup ? 'Sign Up' : 'Login'}
</button>
<button
type="button"
onClick={() => {
setIsSignup(!isSignup);
setError('');
setConfirmPassword('');
}}
className="w-full p-2 bg-gray-600 hover:bg-gray-700 rounded"
>
{isSignup ? 'Already have an account? Login' : 'Need an account? Sign Up'}
</button>
</form>
</div>
);
}