mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat: integrate MarkdownEditor component and update ProfileGeneralTab for enhanced description input
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import Textarea from '../ui/TextArea';
|
||||
import MarkdownEditor from '@ui/MarkdownEditor';
|
||||
|
||||
const ProfileGeneralTab = ({
|
||||
name,
|
||||
@@ -39,7 +39,7 @@ const ProfileGeneralTab = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<div className='space-y-6'>
|
||||
<div className='space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='space-y-1'>
|
||||
@@ -94,20 +94,14 @@ const ProfileGeneralTab = ({
|
||||
</label>
|
||||
<p className='text-xs text-gray-500 dark:text-gray-400'>
|
||||
Add any notes or details about this profile's
|
||||
purpose and configuration
|
||||
purpose and configuration. Use markdown to format
|
||||
your description.
|
||||
</p>
|
||||
</div>
|
||||
<Textarea
|
||||
<MarkdownEditor
|
||||
value={description}
|
||||
onChange={e => onDescriptionChange(e.target.value)}
|
||||
placeholder='Enter a description for this profile'
|
||||
rows={4}
|
||||
className='w-full rounded-md border border-gray-300 dark:border-gray-600
|
||||
bg-white dark:bg-gray-700
|
||||
text-gray-900 dark:text-gray-100
|
||||
placeholder-gray-500 dark:placeholder-gray-400
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent
|
||||
transition-colors duration-200'
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -142,7 +136,7 @@ const ProfileGeneralTab = ({
|
||||
</button>
|
||||
</div>
|
||||
{tags.length > 0 ? (
|
||||
<div className='flex flex-wrap gap-2 rounded-md '>
|
||||
<div className='flex flex-wrap gap-2 rounded-md'>
|
||||
{tags.map(tag => (
|
||||
<span
|
||||
key={tag}
|
||||
|
||||
157
frontend/src/components/ui/MarkdownEditor.jsx
Normal file
157
frontend/src/components/ui/MarkdownEditor.jsx
Normal file
@@ -0,0 +1,157 @@
|
||||
import React, {useState} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
Bold,
|
||||
Italic,
|
||||
List,
|
||||
ListOrdered,
|
||||
Code,
|
||||
Link,
|
||||
Quote,
|
||||
Eye,
|
||||
Edit2
|
||||
} from 'lucide-react';
|
||||
import Textarea from '../ui/TextArea';
|
||||
import ReactMarkdown from 'react-markdown';
|
||||
|
||||
const MarkdownEditor = ({value, onChange, placeholder}) => {
|
||||
const [isPreview, setIsPreview] = useState(false);
|
||||
|
||||
const insertMarkdown = (prefix, suffix = '') => {
|
||||
const textarea = document.querySelector('#markdown-textarea');
|
||||
const start = textarea.selectionStart;
|
||||
const end = textarea.selectionEnd;
|
||||
const selectedText = value.substring(start, end);
|
||||
|
||||
const beforeText = value.substring(0, start);
|
||||
const afterText = value.substring(end);
|
||||
|
||||
const newText = selectedText
|
||||
? `${beforeText}${prefix}${selectedText}${suffix}${afterText}`
|
||||
: `${beforeText}${prefix}placeholder${suffix}${afterText}`;
|
||||
|
||||
onChange({target: {value: newText}});
|
||||
|
||||
setTimeout(() => {
|
||||
textarea.focus();
|
||||
const newPosition = selectedText
|
||||
? start + prefix.length + selectedText.length + suffix.length
|
||||
: start + prefix.length + 'placeholder'.length;
|
||||
textarea.setSelectionRange(newPosition, newPosition);
|
||||
}, 0);
|
||||
};
|
||||
|
||||
const controls = [
|
||||
{
|
||||
icon: Bold,
|
||||
label: 'Bold',
|
||||
action: () => insertMarkdown('**', '**')
|
||||
},
|
||||
{
|
||||
icon: Italic,
|
||||
label: 'Italic',
|
||||
action: () => insertMarkdown('*', '*')
|
||||
},
|
||||
{
|
||||
icon: Code,
|
||||
label: 'Code',
|
||||
action: () => insertMarkdown('`', '`')
|
||||
},
|
||||
{
|
||||
icon: Link,
|
||||
label: 'Link',
|
||||
action: () => insertMarkdown('[', '](url)')
|
||||
},
|
||||
{
|
||||
icon: ListOrdered,
|
||||
label: 'Numbered List',
|
||||
action: () => insertMarkdown('\n1. ')
|
||||
},
|
||||
{
|
||||
icon: List,
|
||||
label: 'Bullet List',
|
||||
action: () => insertMarkdown('\n- ')
|
||||
},
|
||||
{
|
||||
icon: Quote,
|
||||
label: 'Quote',
|
||||
action: () => insertMarkdown('\n> ')
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<div className='rounded-md border border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 overflow-hidden'>
|
||||
{/* Markdown Controls */}
|
||||
<div className='flex items-center gap-1 p-2 border-b border-gray-300 dark:border-gray-600'>
|
||||
{isPreview ? (
|
||||
<div className='flex items-center gap-2 text-gray-700 dark:text-gray-300 ml-2'>
|
||||
<Eye className='w-4 h-4' />
|
||||
<span className='text-sm font-medium'>
|
||||
Preview Mode
|
||||
</span>
|
||||
</div>
|
||||
) : (
|
||||
controls.map(control => (
|
||||
<button
|
||||
key={control.label}
|
||||
onClick={control.action}
|
||||
className='p-1.5 rounded-md hover:bg-gray-200 dark:hover:bg-gray-600 text-gray-700 dark:text-gray-300 transition-colors'
|
||||
title={control.label}>
|
||||
<control.icon className='w-4 h-4' />
|
||||
</button>
|
||||
))
|
||||
)}
|
||||
|
||||
{/* Preview Toggle */}
|
||||
<div className='ml-auto'>
|
||||
<button
|
||||
onClick={() => setIsPreview(!isPreview)}
|
||||
className='flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 rounded transition-colors'>
|
||||
{isPreview ? (
|
||||
<>
|
||||
<Edit2 className='w-4 h-4' />
|
||||
Edit
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Eye className='w-4 h-4' />
|
||||
Preview
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Editor/Preview Area */}
|
||||
<div>
|
||||
{isPreview ? (
|
||||
<div className='px-4 py-3 min-h-[300px] h-full prose prose-sm dark:prose-invert max-w-none bg-white dark:bg-gray-700'>
|
||||
{value ? (
|
||||
<ReactMarkdown>{value}</ReactMarkdown>
|
||||
) : (
|
||||
<p className='text-gray-500 dark:text-gray-400 italic'>
|
||||
Nothing to preview
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Textarea
|
||||
id='markdown-textarea'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
placeholder={placeholder}
|
||||
rows={12}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MarkdownEditor.propTypes = {
|
||||
value: PropTypes.string.isRequired,
|
||||
onChange: PropTypes.func.isRequired,
|
||||
placeholder: PropTypes.string
|
||||
};
|
||||
|
||||
export default MarkdownEditor;
|
||||
@@ -1,15 +1,18 @@
|
||||
import React from "react";
|
||||
|
||||
const Textarea = React.forwardRef(({ className, ...props }, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={`flex min-h-[80px] w-full rounded-md border border-dark-border bg-dark-card px-3 py-2 text-sm text-dark-text placeholder-gray-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
import React from 'react';
|
||||
const Textarea = React.forwardRef(({className, ...props}, ref) => {
|
||||
return (
|
||||
<textarea
|
||||
className={`flex min-h-[300px] w-full border-none bg-white
|
||||
dark:bg-gray-700 px-4 py-3 text-sm text-gray-900 dark:text-gray-100
|
||||
placeholder-gray-500 dark:placeholder-gray-400
|
||||
focus:outline-none focus:ring-2 focus:ring-blue-500
|
||||
focus:border-transparent disabled:cursor-not-allowed
|
||||
disabled:opacity-50 rounded-none ${className}`}
|
||||
ref={ref}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
});
|
||||
Textarea.displayName = 'Textarea';
|
||||
|
||||
Textarea.displayName = "Textarea";
|
||||
|
||||
export default Textarea;
|
||||
export default Textarea;
|
||||
|
||||
Reference in New Issue
Block a user