Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
152 changes: 149 additions & 3 deletions app/admin/settings/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";
import React, { useState, useEffect } from 'react';
import { supabase } from '../../lib/supabaseClient';
import { Settings, Shield, Users, FileText, Bell, Database, Lock, Save, Eye, EyeOff } from 'lucide-react';
import { Settings, Shield, Users, FileText, Bell, Database, Lock, Save, Eye, EyeOff, FileEdit } from 'lucide-react';
import { toast, ToastContainer } from 'react-toastify';
import 'react-toastify/dist/ReactToastify.css';
import { useAuth } from '../../context/AuthContext';
Expand All @@ -19,6 +19,14 @@ interface AdminSettings {
contact_email: string;
}

interface TermsAndConditions {
id: string | null;
title: string;
content: string;
version: number;
last_updated: string;
}

interface SystemStats {
database_size: string;
storage_used: string;
Expand All @@ -43,15 +51,19 @@ const AdminSettingsPage = () => {
const [systemStats, setSystemStats] = useState<SystemStats | null>(null);
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [activeTab, setActiveTab] = useState<'general' | 'listings' | 'users' | 'system' | 'security'>('general');
const [activeTab, setActiveTab] = useState<'general' | 'listings' | 'users' | 'system' | 'security' | 'terms'>('general');
const [newAdminEmail, setNewAdminEmail] = useState('');
const [adminUsers, setAdminUsers] = useState<any[]>([]);
const [showApiKeys, setShowApiKeys] = useState(false);
const [terms, setTerms] = useState<TermsAndConditions | null>(null);
const [termsLoading, setTermsLoading] = useState(false);
const [termsSaving, setTermsSaving] = useState(false);

useEffect(() => {
fetchSettings();
fetchSystemStats();
fetchAdminUsers();
fetchTerms();
}, []);

const fetchSettings = async () => {
Expand Down Expand Up @@ -99,6 +111,64 @@ const AdminSettingsPage = () => {
}
};

const fetchTerms = async () => {
try {
setTermsLoading(true);
const response = await fetch('/api/terms');
if (!response.ok) {
throw new Error('Failed to fetch terms');
}
const data = await response.json();
setTerms(data);
} catch (error) {
console.error('Error fetching terms:', error);
toast.error('Failed to load terms and conditions');
} finally {
setTermsLoading(false);
}
};

const saveTerms = async () => {
if (!terms) return;

try {
setTermsSaving(true);

// Get the current session token
const { data: { session } } = await supabase.auth.getSession();
if (!session) {
toast.error('You must be logged in to save terms');
return;
}

const response = await fetch('/api/terms', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${session.access_token}`,
},
body: JSON.stringify({
title: terms.title,
content: terms.content,
}),
});

if (!response.ok) {
const errorData = await response.json();
throw new Error(errorData.error || 'Failed to save terms');
}

const updatedTerms = await response.json();
setTerms(updatedTerms);
toast.success('Terms and conditions updated successfully');
} catch (error) {
console.error('Error saving terms:', error);
toast.error(error instanceof Error ? error.message : 'Failed to save terms and conditions');
} finally {
setTermsSaving(false);
}
};

const fetchAdminUsers = async () => {
try {
// First try to fetch with is_admin column
Expand Down Expand Up @@ -246,7 +316,8 @@ const AdminSettingsPage = () => {
{ id: 'listings', label: 'Listings', icon: FileText },
{ id: 'users', label: 'Users', icon: Users },
{ id: 'system', label: 'System', icon: Database },
{ id: 'security', label: 'Security', icon: Lock }
{ id: 'security', label: 'Security', icon: Lock },
{ id: 'terms', label: 'Terms', icon: FileEdit }
] as const;

if (loading) {
Expand Down Expand Up @@ -606,6 +677,81 @@ const AdminSettingsPage = () => {
</div>
</div>
)}

{/* Terms and Conditions Editor */}
{activeTab === 'terms' && (
<div className="space-y-6">
<div>
<h3 className="text-lg font-medium text-gray-900 mb-4">Terms and Conditions Editor</h3>

{termsLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-[#bf5700]"></div>
<span className="ml-2 text-gray-600">Loading terms...</span>
</div>
) : terms ? (
<div className="space-y-4">
<div className="bg-gray-50 rounded-lg p-4 border border-gray-200">
<div className="flex items-center justify-between mb-2">
<h4 className="font-medium text-gray-900">Current Version</h4>
<span className="text-sm text-gray-500">v{terms.version}</span>
</div>
<p className="text-sm text-gray-600">
Last updated: {new Date(terms.last_updated).toLocaleString()}
</p>
</div>

<div>
<label className="block text-sm font-medium text-gray-900 mb-2">Title</label>
<input
type="text"
value={terms.title}
onChange={(e) => setTerms({...terms, title: e.target.value})}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#bf5700] focus:border-transparent"
placeholder="Terms and Conditions"
/>
</div>

<div>
<label className="block text-sm font-medium text-gray-900 mb-2">Content (Markdown supported)</label>
<textarea
value={terms.content}
onChange={(e) => setTerms({...terms, content: e.target.value})}
rows={20}
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#bf5700] focus:border-transparent font-mono text-sm"
placeholder="Enter terms and conditions content..."
/>
<p className="text-xs text-gray-500 mt-1">
You can use Markdown formatting. The content will be displayed in the signup modal.
</p>
</div>

<div className="flex justify-end">
<button
onClick={saveTerms}
disabled={termsSaving}
className="flex items-center px-6 py-2 bg-[#bf5700] text-white rounded-lg hover:bg-[#a54700] transition-colors disabled:opacity-50"
>
<Save size={16} className="mr-2" />
{termsSaving ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
) : (
<div className="text-center py-8">
<FileEdit size={48} className="mx-auto text-gray-400 mb-4" />
<p className="text-gray-600">Failed to load terms and conditions</p>
<button
onClick={fetchTerms}
className="mt-2 px-4 py-2 bg-[#bf5700] text-white rounded-lg hover:bg-[#a54700] transition-colors"
>
Retry
</button>
</div>
)}
</div>
</div>
)}
</div>
</div>

Expand Down
124 changes: 124 additions & 0 deletions app/api/terms/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import { NextRequest, NextResponse } from 'next/server';
import { supabase } from '../../lib/supabaseClient';

// GET /api/terms - Fetch current terms and conditions
export async function GET() {
try {
const { data, error } = await supabase
.from('terms_and_conditions')
.select('*')
.order('created_at', { ascending: false })
.limit(1)
.single();

if (error) {
// If no terms exist, return default terms
if (error.code === 'PGRST116') {
return NextResponse.json({
id: null,
title: 'Terms and Conditions',
content: 'No terms and conditions found',
last_updated: new Date().toISOString(),
version: 1
});
}
throw error;
}

return NextResponse.json(data);
} catch (error) {
console.error('Error fetching terms:', error);
return NextResponse.json(
{ error: 'Failed to fetch terms and conditions' },
{ status: 500 }
);
}
}

// POST /api/terms - Update terms and conditions (admin only)
export async function POST(request: NextRequest) {
try {
const body = await request.json();
const { title, content } = body;

if (!title || !content) {
return NextResponse.json(
{ error: 'Title and content are required' },
{ status: 400 }
);
}

// Get the authorization header
const authHeader = request.headers.get('authorization');
if (!authHeader) {
return NextResponse.json(
{ error: 'Authentication required' },
{ status: 401 }
);
}

// Extract the token from the header
const token = authHeader.replace('Bearer ', '');

// Verify the user is authenticated and is an admin
const { data: { user }, error: authError } = await supabase.auth.getUser(token);

if (authError || !user) {
return NextResponse.json(
{ error: 'Invalid authentication' },
{ status: 401 }
);
}

// Check if user is admin
const { data: userProfile, error: profileError } = await supabase
.from('users')
.select('is_admin')
.eq('id', user.id)
.single();

if (profileError || !userProfile?.is_admin) {
return NextResponse.json(
{ error: 'Admin privileges required' },
{ status: 403 }
);
}

// Insert new terms version
const { data, error } = await supabase
.from('terms_and_conditions')
.insert({
title,
content,
version: await getNextVersion(),
last_updated: new Date().toISOString(),
created_by: user.id
})
.select()
.single();

if (error) {
throw error;
}

return NextResponse.json(data);
} catch (error) {
console.error('Error updating terms:', error);
return NextResponse.json(
{ error: 'Failed to update terms and conditions' },
{ status: 500 }
);
}
}

// Helper function to get next version number
async function getNextVersion(): Promise<number> {
const { data } = await supabase
.from('terms_and_conditions')
.select('version')
.order('version', { ascending: false })
.limit(1)
.single();

return (data?.version || 0) + 1;
}
7 changes: 5 additions & 2 deletions app/auth/signin/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,8 @@ export default function SignIn() {
animate={{ opacity: 1, height: 'auto' }}
exit={{ opacity: 0, height: 0 }}
transition={{ duration: 0.3, ease: "easeInOut" }}
className="overflow-hidden"
className="overflow-hidden mt-4"

>
<motion.div
initial={{ opacity: 0, y: 10 }}
Expand Down Expand Up @@ -597,7 +598,9 @@ export default function SignIn() {
{/* Terms and Conditions Modal */}
<TermsModal
isOpen={showTermsModal}
onClose={() => setShowTermsModal(false)}
onClose={() => setShowTermsModal(false)}
onAccept={() => setTermsAccepted(true)}

/>
</div>
);
Expand Down
Loading