@diagramers/admin
Version:
Diagramers Admin Template - React starter for admin dashboards.
446 lines (417 loc) • 15.2 kB
JavaScript
import React, { useState, useRef, useEffect } from 'react';
import { Row, Col, Card, Form, Button, Alert, Modal } from 'react-bootstrap';
import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useDispatch } from 'react-redux';
import { settingsChangeColor } from 'settings/settingsSlice';
import { THEME_COLOR } from 'constants.js';
import CsLineIcons from 'cs-line-icons/CsLineIcons';
import configService from 'services/configService';
import './ProjectSettings.css';
const ProjectSettings = () => {
const dispatch = useDispatch();
const fileInputRef = useRef(null);
const [logoPreview, setLogoPreview] = useState(null);
const [isSubmitting, setIsSubmitting] = useState(false);
const [showResetModal, setShowResetModal] = useState(false);
const [currentConfig, setCurrentConfig] = useState(null);
// Available themes with preview colors
const availableThemes = [
{
key: THEME_COLOR.LightBlue,
name: 'Light Blue',
color: '#1ea8e7',
description: 'Professional and clean blue theme'
},
{
key: THEME_COLOR.DarkBlue,
name: 'Dark Blue',
color: '#1ea8e7',
description: 'Modern dark blue theme'
},
{
key: THEME_COLOR.LightRed,
name: 'Light Red',
color: '#cf2637',
description: 'Energetic red theme'
},
{
key: THEME_COLOR.DarkRed,
name: 'Dark Red',
color: '#cf2637',
description: 'Bold dark red theme'
},
{
key: THEME_COLOR.LightGreen,
name: 'Light Green',
color: '#439b38',
description: 'Fresh green theme'
},
{
key: THEME_COLOR.DarkGreen,
name: 'Dark Green',
color: '#439b38',
description: 'Natural dark green theme'
},
{
key: THEME_COLOR.LightPurple,
name: 'Light Purple',
color: '#6f42c1',
description: 'Creative purple theme'
},
{
key: THEME_COLOR.DarkPurple,
name: 'Dark Purple',
color: '#6f42c1',
description: 'Elegant dark purple theme'
},
{
key: THEME_COLOR.LightPink,
name: 'Light Pink',
color: '#e83e8c',
description: 'Playful pink theme'
},
{
key: THEME_COLOR.DarkPink,
name: 'Dark Pink',
color: '#e83e8c',
description: 'Vibrant dark pink theme'
},
];
const validationSchema = Yup.object().shape({
projectName: Yup.string()
.min(2, 'Project name must be at least 2 characters')
.max(50, 'Project name must be less than 50 characters')
.required('Project name is required'),
defaultTheme: Yup.string()
.oneOf(Object.values(THEME_COLOR), 'Please select a valid theme')
.required('Default theme is required'),
});
// Load current configuration on component mount
useEffect(() => {
const config = configService.getConfig();
if (config) {
setCurrentConfig(config);
setLogoPreview(config.logo);
}
}, []);
const formik = useFormik({
initialValues: {
projectName: currentConfig?.projectName || '',
defaultTheme: currentConfig?.defaultTheme || THEME_COLOR.LightBlue,
},
validationSchema,
enableReinitialize: true,
onSubmit: async (values) => {
setIsSubmitting(true);
try {
// Update project configuration
const success = configService.updateConfig({
projectName: values.projectName,
defaultTheme: values.defaultTheme,
logo: logoPreview,
});
if (success) {
// Update theme
dispatch(settingsChangeColor(values.defaultTheme));
// Update document title
document.title = values.projectName;
// Update current config
setCurrentConfig(configService.getConfig());
alert('Project settings updated successfully!');
} else {
throw new Error('Failed to update configuration');
}
} catch (error) {
console.error('Update failed:', error);
alert('Failed to update project settings. Please try again.');
} finally {
setIsSubmitting(false);
}
},
});
const handleLogoUpload = (event) => {
const file = event.target.files[0];
if (file) {
// Validate file type
if (!file.type.startsWith('image/')) {
alert('Please select an image file');
return;
}
// Validate file size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
alert('Logo file size must be less than 2MB');
return;
}
const reader = new FileReader();
reader.onload = (e) => {
setLogoPreview(e.target.result);
};
reader.readAsDataURL(file);
}
};
const removeLogo = () => {
setLogoPreview(null);
if (fileInputRef.current) {
fileInputRef.current.value = '';
}
};
const handleResetConfig = () => {
configService.resetConfig();
setCurrentConfig(null);
setLogoPreview(null);
setShowResetModal(false);
window.location.reload(); // Reload to trigger setup flow
};
const handleExportConfig = () => {
configService.exportConfig();
};
const handleImportConfig = (event) => {
const file = event.target.files[0];
if (file) {
configService.importConfig(file)
.then((config) => {
setCurrentConfig(config);
setLogoPreview(config.logo);
formik.setValues({
projectName: config.projectName || '',
defaultTheme: config.defaultTheme || THEME_COLOR.LightBlue,
});
alert('Configuration imported successfully!');
})
.catch((error) => {
alert(`Failed to import configuration: ${error.message}`);
});
}
};
if (!currentConfig) {
return (
<div className="d-flex justify-content-center align-items-center" style={{ height: '50vh' }}>
<div className="spinner-border text-primary" role="status">
<span className="visually-hidden">Loading...</span>
</div>
</div>
);
}
return (
<>
<Row>
<Col>
<div className="page-title-container">
<h1 className="mb-0 pb-0 display-4">Project Settings</h1>
<p className="text-muted">Configure your project logo, name, and theme</p>
</div>
</Col>
</Row>
<Row>
<Col lg="8">
<Card>
<Card.Body>
<Form onSubmit={formik.handleSubmit}>
{/* Project Name */}
<Form.Group className="mb-4">
<Form.Label className="fw-bold">Project Name</Form.Label>
<Form.Control
type="text"
name="projectName"
placeholder="Enter your project name"
value={formik.values.projectName}
onChange={formik.handleChange}
onBlur={formik.handleBlur}
isInvalid={formik.touched.projectName && formik.errors.projectName}
/>
<Form.Control.Feedback type="invalid">
{formik.errors.projectName}
</Form.Control.Feedback>
<Form.Text className="text-muted">
This name will be displayed in the header and browser tab
</Form.Text>
</Form.Group>
{/* Logo Upload */}
<Form.Group className="mb-4">
<Form.Label className="fw-bold">Project Logo</Form.Label>
<div className="logo-upload-area">
{logoPreview ? (
<div className="logo-preview-container">
<img
src={logoPreview}
alt="Logo preview"
className="logo-preview"
/>
<Button
variant="outline-danger"
size="sm"
onClick={removeLogo}
className="remove-logo-btn"
>
<CsLineIcons icon="trash" />
</Button>
</div>
) : (
<div
className="logo-upload-placeholder"
onClick={() => fileInputRef.current?.click()}
>
<CsLineIcons icon="upload" className="text-muted" size="32" />
<p className="mb-0 mt-2">Click to upload logo</p>
<small className="text-muted">PNG, JPG, SVG (max 2MB)</small>
</div>
)}
<input
ref={fileInputRef}
type="file"
accept="image/*"
onChange={handleLogoUpload}
style={{ display: 'none' }}
/>
</div>
<Form.Text className="text-muted">
Upload your project logo (recommended: 200x200px)
</Form.Text>
</Form.Group>
{/* Theme Selection */}
<Form.Group className="mb-4">
<Form.Label className="fw-bold">Default Theme</Form.Label>
<div className="theme-grid">
{availableThemes.map((theme) => (
<div
key={theme.key}
className={`theme-option ${formik.values.defaultTheme === theme.key ? 'selected' : ''}`}
onClick={() => formik.setFieldValue('defaultTheme', theme.key)}
>
<div
className="theme-color-preview"
style={{ backgroundColor: theme.color }}
/>
<div className="theme-info">
<h6 className="theme-name">{theme.name}</h6>
<p className="theme-description">{theme.description}</p>
</div>
{formik.values.defaultTheme === theme.key && (
<div className="theme-selected">
<CsLineIcons icon="check" className="text-white" />
</div>
)}
</div>
))}
</div>
{formik.touched.defaultTheme && formik.errors.defaultTheme && (
<div className="text-danger mt-2">{formik.errors.defaultTheme}</div>
)}
</Form.Group>
{/* Submit Button */}
<div className="d-flex gap-2">
<Button
type="submit"
disabled={isSubmitting || !formik.isValid}
className="flex-fill"
>
{isSubmitting ? (
<>
<span className="spinner-border spinner-border-sm me-2" />
Updating...
</>
) : (
<>
<CsLineIcons icon="check" className="me-2" />
Update Settings
</>
)}
</Button>
</div>
</Form>
</Card.Body>
</Card>
</Col>
<Col lg="4">
<Card>
<Card.Header>
<h5 className="mb-0">Configuration Tools</h5>
</Card.Header>
<Card.Body>
<div className="d-grid gap-2">
<Button
variant="outline-primary"
onClick={handleExportConfig}
className="d-flex align-items-center justify-content-center"
>
<CsLineIcons icon="download" className="me-2" />
Export Configuration
</Button>
<Button
variant="outline-secondary"
onClick={() => document.getElementById('import-config').click()}
className="d-flex align-items-center justify-content-center"
>
<CsLineIcons icon="upload" className="me-2" />
Import Configuration
</Button>
<Button
variant="outline-danger"
onClick={() => setShowResetModal(true)}
className="d-flex align-items-center justify-content-center"
>
<CsLineIcons icon="trash" className="me-2" />
Reset Configuration
</Button>
</div>
<input
id="import-config"
type="file"
accept=".json"
onChange={handleImportConfig}
style={{ display: 'none' }}
/>
</Card.Body>
</Card>
<Card className="mt-3">
<Card.Header>
<h5 className="mb-0">Current Configuration</h5>
</Card.Header>
<Card.Body>
<div className="mb-2">
<strong>Project Name:</strong>
<p className="text-muted mb-0">{currentConfig.projectName}</p>
</div>
<div className="mb-2">
<strong>Theme:</strong>
<p className="text-muted mb-0">{currentConfig.defaultTheme}</p>
</div>
<div className="mb-2">
<strong>Setup Date:</strong>
<p className="text-muted mb-0">
{new Date(currentConfig.setupDate).toLocaleDateString()}
</p>
</div>
</Card.Body>
</Card>
</Col>
</Row>
{/* Reset Configuration Modal */}
<Modal show={showResetModal} onHide={() => setShowResetModal(false)}>
<Modal.Header closeButton>
<Modal.Title>Reset Configuration</Modal.Title>
</Modal.Header>
<Modal.Body>
<p>Are you sure you want to reset your project configuration? This will:</p>
<ul>
<li>Remove your project logo</li>
<li>Reset project name to default</li>
<li>Reset theme to default</li>
<li>Redirect you to the setup page</li>
</ul>
<p className="text-danger"><strong>This action cannot be undone.</strong></p>
</Modal.Body>
<Modal.Footer>
<Button variant="secondary" onClick={() => setShowResetModal(false)}>
Cancel
</Button>
<Button variant="danger" onClick={handleResetConfig}>
Reset Configuration
</Button>
</Modal.Footer>
</Modal>
</>
);
};
export default ProjectSettings;