Merge from dev to loaf-prod for DEMO #25
92
src/hooks/use-directory-config.js
Normal file
92
src/hooks/use-directory-config.js
Normal file
@@ -0,0 +1,92 @@
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import api from '../utils/api';
|
||||
|
||||
/**
|
||||
* Default directory configuration - used as fallback if API fails
|
||||
*/
|
||||
const DEFAULT_DIRECTORY_CONFIG = {
|
||||
fields: {
|
||||
show_in_directory: { enabled: true, label: 'Show in Directory', required: false },
|
||||
directory_email: { enabled: true, label: 'Directory Email', required: false },
|
||||
directory_bio: { enabled: true, label: 'Bio', required: false },
|
||||
directory_address: { enabled: true, label: 'Address', required: false },
|
||||
directory_phone: { enabled: true, label: 'Phone', required: false },
|
||||
directory_dob: { enabled: true, label: 'Birthday', required: false },
|
||||
directory_partner_name: { enabled: true, label: 'Partner Name', required: false },
|
||||
volunteer_interests: { enabled: true, label: 'Volunteer Interests', required: false },
|
||||
social_media: { enabled: true, label: 'Social Media Links', required: false },
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook to fetch and manage directory field configuration
|
||||
* @returns {Object} - { config, loading, error, isFieldEnabled, getFieldLabel, refetch }
|
||||
*/
|
||||
const useDirectoryConfig = () => {
|
||||
const [config, setConfig] = useState(DEFAULT_DIRECTORY_CONFIG);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState(null);
|
||||
|
||||
const fetchConfig = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const response = await api.get('/directory/config');
|
||||
setConfig(response.data || DEFAULT_DIRECTORY_CONFIG);
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch directory config:', err);
|
||||
setError(err);
|
||||
// Use default config on error
|
||||
setConfig(DEFAULT_DIRECTORY_CONFIG);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, [fetchConfig]);
|
||||
|
||||
/**
|
||||
* Check if a field is enabled in the config
|
||||
* @param {string} fieldId - The field ID to check (e.g., 'directory_email')
|
||||
* @returns {boolean} - Whether the field is enabled
|
||||
*/
|
||||
const isFieldEnabled = useCallback((fieldId) => {
|
||||
const field = config?.fields?.[fieldId];
|
||||
return field?.enabled !== false; // Default to true if not specified
|
||||
}, [config]);
|
||||
|
||||
/**
|
||||
* Get the label for a field
|
||||
* @param {string} fieldId - The field ID
|
||||
* @param {string} defaultLabel - Default label if not in config
|
||||
* @returns {string} - The field label
|
||||
*/
|
||||
const getFieldLabel = useCallback((fieldId, defaultLabel = '') => {
|
||||
const field = config?.fields?.[fieldId];
|
||||
return field?.label || defaultLabel;
|
||||
}, [config]);
|
||||
|
||||
/**
|
||||
* Check if a field is required
|
||||
* @param {string} fieldId - The field ID
|
||||
* @returns {boolean} - Whether the field is required
|
||||
*/
|
||||
const isFieldRequired = useCallback((fieldId) => {
|
||||
const field = config?.fields?.[fieldId];
|
||||
return field?.required === true;
|
||||
}, [config]);
|
||||
|
||||
return {
|
||||
config,
|
||||
loading,
|
||||
error,
|
||||
isFieldEnabled,
|
||||
getFieldLabel,
|
||||
isFieldRequired,
|
||||
refetch: fetchConfig
|
||||
};
|
||||
};
|
||||
|
||||
export default useDirectoryConfig;
|
||||
241
src/pages/admin/AdminDirectorySettings.js
Normal file
241
src/pages/admin/AdminDirectorySettings.js
Normal file
@@ -0,0 +1,241 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import api from '../../utils/api';
|
||||
import { Card } from '../../components/ui/card';
|
||||
import { Button } from '../../components/ui/button';
|
||||
import { Switch } from '../../components/ui/switch';
|
||||
import { Label } from '../../components/ui/label';
|
||||
import { toast } from 'sonner';
|
||||
import { BookUser, Save, RotateCcw, Loader2, AlertCircle } from 'lucide-react';
|
||||
import {
|
||||
Alert,
|
||||
AlertDescription,
|
||||
} from '../../components/ui/alert';
|
||||
|
||||
const AdminDirectorySettings = () => {
|
||||
const [config, setConfig] = useState(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [hasChanges, setHasChanges] = useState(false);
|
||||
const [initialConfig, setInitialConfig] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
fetchConfig();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (initialConfig && config) {
|
||||
const changed = JSON.stringify(config) !== JSON.stringify(initialConfig);
|
||||
setHasChanges(changed);
|
||||
}
|
||||
}, [config, initialConfig]);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await api.get('/admin/directory/config');
|
||||
setConfig(response.data.config);
|
||||
setInitialConfig(response.data.config);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch directory config:', error);
|
||||
toast.error('Failed to load directory configuration');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleToggleField = (fieldId) => {
|
||||
// Don't allow disabling show_in_directory - it's required
|
||||
if (fieldId === 'show_in_directory') {
|
||||
toast.error('The "Show in Directory" field cannot be disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
setConfig(prev => ({
|
||||
...prev,
|
||||
fields: {
|
||||
...prev.fields,
|
||||
[fieldId]: {
|
||||
...prev.fields[fieldId],
|
||||
enabled: !prev.fields[fieldId].enabled
|
||||
}
|
||||
}
|
||||
}));
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
await api.put('/admin/directory/config', { config });
|
||||
setInitialConfig(config);
|
||||
setHasChanges(false);
|
||||
toast.success('Directory configuration saved successfully');
|
||||
} catch (error) {
|
||||
console.error('Failed to save directory config:', error);
|
||||
toast.error('Failed to save directory configuration');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleReset = async () => {
|
||||
if (!window.confirm('Are you sure you want to reset to default settings? This will enable all directory fields.')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setSaving(true);
|
||||
const response = await api.post('/admin/directory/config/reset');
|
||||
setConfig(response.data.config);
|
||||
setInitialConfig(response.data.config);
|
||||
setHasChanges(false);
|
||||
toast.success('Directory configuration reset to defaults');
|
||||
} catch (error) {
|
||||
console.error('Failed to reset directory config:', error);
|
||||
toast.error('Failed to reset directory configuration');
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setConfig(initialConfig);
|
||||
setHasChanges(false);
|
||||
};
|
||||
|
||||
// Field descriptions for better UX
|
||||
const fieldDescriptions = {
|
||||
show_in_directory: 'Master toggle for members to opt-in to the directory (always enabled)',
|
||||
directory_email: 'Email address visible to other members in the directory',
|
||||
directory_bio: 'Short biography shown in directory profile and member cards',
|
||||
directory_address: 'Physical address visible to other members',
|
||||
directory_phone: 'Phone number visible to other members',
|
||||
directory_dob: 'Birthday shown in directory profiles',
|
||||
directory_partner_name: 'Partner name displayed in directory',
|
||||
volunteer_interests: 'Volunteer interest badges shown in profiles',
|
||||
social_media: 'Social media links (Facebook, Instagram, Twitter, LinkedIn)',
|
||||
};
|
||||
|
||||
// Field icons for better UX
|
||||
const fieldLabels = {
|
||||
show_in_directory: 'Show in Directory Toggle',
|
||||
directory_email: 'Directory Email',
|
||||
directory_bio: 'Bio / About',
|
||||
directory_address: 'Address',
|
||||
directory_phone: 'Phone Number',
|
||||
directory_dob: 'Birthday',
|
||||
directory_partner_name: 'Partner Name',
|
||||
volunteer_interests: 'Volunteer Interests',
|
||||
social_media: 'Social Media Links',
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[400px]">
|
||||
<Loader2 className="h-8 w-8 animate-spin text-brand-purple" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-2xl font-semibold text-foreground flex items-center gap-2">
|
||||
<BookUser className="h-6 w-6 text-brand-purple" />
|
||||
Directory Field Settings
|
||||
</h2>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Configure which fields are available in member profiles and the directory
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Info Alert */}
|
||||
<Alert>
|
||||
<AlertCircle className="h-4 w-4" />
|
||||
<AlertDescription>
|
||||
These settings control which fields appear in the <strong>Profile page</strong> and <strong>Member Directory</strong>.
|
||||
Disabling a field will hide it from both locations. Existing data will be preserved but not displayed.
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
{/* Fields Configuration */}
|
||||
<Card className="p-6">
|
||||
<div className="space-y-6">
|
||||
{config && Object.entries(config.fields).map(([fieldId, fieldData]) => (
|
||||
<div
|
||||
key={fieldId}
|
||||
className={`flex items-center justify-between p-4 rounded-lg border ${
|
||||
fieldData.enabled ? 'bg-background border-border' : 'bg-muted/50 border-muted'
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Label className="text-base font-medium text-foreground">
|
||||
{fieldLabels[fieldId] || fieldData.label || fieldId}
|
||||
</Label>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{fieldDescriptions[fieldId] || fieldData.description}
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
checked={fieldData.enabled}
|
||||
onCheckedChange={() => handleToggleField(fieldId)}
|
||||
disabled={fieldId === 'show_in_directory'}
|
||||
className="data-[state=checked]:bg-brand-purple"
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex items-center justify-between pt-4 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleReset}
|
||||
disabled={saving}
|
||||
className="text-muted-foreground"
|
||||
>
|
||||
<RotateCcw className="h-4 w-4 mr-2" />
|
||||
Reset to Defaults
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{hasChanges && (
|
||||
<span className="text-sm text-muted-foreground flex items-center gap-2">
|
||||
<span className="h-2 w-2 rounded-full bg-orange-500"></span>
|
||||
Unsaved changes
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleCancel}
|
||||
disabled={!hasChanges || saving}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSave}
|
||||
disabled={!hasChanges || saving}
|
||||
className="bg-brand-purple hover:bg-brand-purple/90 text-white"
|
||||
>
|
||||
{saving ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Changes
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminDirectorySettings;
|
||||
Reference in New Issue
Block a user