UNPKG

@diagramers/admin

Version:

Diagramers Admin Template - React starter for admin dashboards.

446 lines (417 loc) 15.2 kB
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;