mirror of
https://github.com/philipredstone/relnet.git
synced 2025-07-09 07:06:41 +02:00
ach ich weiß nicht
This commit is contained in:
575
src/components/Modals.tsx
Normal file
575
src/components/Modals.tsx
Normal file
@ -0,0 +1,575 @@
|
||||
// Modals.tsx - Modal components for the FriendshipNetwork
|
||||
|
||||
import React from 'react';
|
||||
import DatePicker from 'react-datepicker';
|
||||
import {
|
||||
FaPlus,
|
||||
FaRegCalendarAlt,
|
||||
FaSave,
|
||||
FaStar,
|
||||
FaTrash,
|
||||
FaUserFriends,
|
||||
FaUserPlus,
|
||||
} from 'react-icons/fa';
|
||||
|
||||
import { Button, FormField, Modal } from '../components/FriendshipNetworkComponents';
|
||||
|
||||
import {
|
||||
PersonNode,
|
||||
RelationshipEdge,
|
||||
RelationshipType,
|
||||
FormErrors,
|
||||
NewPersonForm,
|
||||
NewRelationshipForm,
|
||||
} from '../types/types';
|
||||
|
||||
import {
|
||||
KeyboardShortcut,
|
||||
TipItem,
|
||||
ToggleSetting,
|
||||
OptionGroup,
|
||||
ErrorMessage,
|
||||
} from './UIComponents';
|
||||
|
||||
// ==============================
|
||||
// Person Form Modal
|
||||
// ==============================
|
||||
interface PersonFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
formData: NewPersonForm;
|
||||
setFormData: React.Dispatch<React.SetStateAction<NewPersonForm>>;
|
||||
errors: FormErrors;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
isEdit?: boolean;
|
||||
}
|
||||
|
||||
export const PersonFormModal: React.FC<PersonFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
formData,
|
||||
setFormData,
|
||||
errors,
|
||||
onSubmit,
|
||||
isEdit = false,
|
||||
}) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={isEdit ? 'Edit Person' : 'Add New Person'}>
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<ErrorMessage message={errors.general} />
|
||||
|
||||
<FormField label="First Name" id="firstName" required error={errors.firstName}>
|
||||
<input
|
||||
id="firstName"
|
||||
type="text"
|
||||
className={`w-full bg-slate-700 border ${errors.firstName ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
placeholder="Enter first name"
|
||||
value={formData.firstName}
|
||||
onChange={e => setFormData({ ...formData, firstName: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Last Name" id="lastName" required error={errors.lastName}>
|
||||
<input
|
||||
id="lastName"
|
||||
type="text"
|
||||
className={`w-full bg-slate-700 border ${errors.lastName ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
placeholder="Enter last name"
|
||||
value={formData.lastName}
|
||||
onChange={e => setFormData({ ...formData, lastName: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Birthday (Optional)" id="birthday">
|
||||
<div className="relative">
|
||||
<DatePicker
|
||||
id="birthday"
|
||||
selected={formData.birthday}
|
||||
onChange={(date: Date | null) => setFormData({ ...formData, birthday: date })}
|
||||
dateFormat="MMMM d, yyyy"
|
||||
placeholderText="Select birthday"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
showYearDropdown
|
||||
dropdownMode="select"
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
<FaRegCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400" />
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Notes (Optional)" id="notes">
|
||||
<textarea
|
||||
id="notes"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 min-h-[80px]
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
placeholder="Add any additional information"
|
||||
value={formData.notes}
|
||||
onChange={e => setFormData({ ...formData, notes: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" icon={isEdit ? <FaSave /> : <FaUserPlus />}>
|
||||
{isEdit ? 'Save Changes' : 'Add Person'}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// ==============================
|
||||
// Relationship Form Modal
|
||||
// ==============================
|
||||
interface RelationshipFormModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
formData: NewRelationshipForm;
|
||||
setFormData: React.Dispatch<React.SetStateAction<NewRelationshipForm>>;
|
||||
errors: FormErrors;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
people: PersonNode[];
|
||||
relationshipLabels: Record<RelationshipType, string>;
|
||||
}
|
||||
|
||||
export const RelationshipFormModal: React.FC<RelationshipFormModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
formData,
|
||||
setFormData,
|
||||
errors,
|
||||
onSubmit,
|
||||
people,
|
||||
relationshipLabels,
|
||||
}) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Add New Relationship">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<ErrorMessage message={errors.general} />
|
||||
|
||||
<FormField label="Source Person" id="source" required error={errors.source}>
|
||||
<select
|
||||
id="source"
|
||||
className={`w-full bg-slate-700 border ${errors.source ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
value={formData.source}
|
||||
onChange={e => setFormData({ ...formData, source: e.target.value })}
|
||||
>
|
||||
<option value="">Select person</option>
|
||||
{people.map(person => (
|
||||
<option key={`source-${person._id}`} value={person._id}>
|
||||
{person.firstName} {person.lastName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Target Person" id="target" required error={errors.target}>
|
||||
<select
|
||||
id="target"
|
||||
className={`w-full bg-slate-700 border ${errors.target ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
value={formData.target}
|
||||
onChange={e => setFormData({ ...formData, target: e.target.value })}
|
||||
>
|
||||
<option value="">Select person</option>
|
||||
{people.map(person => (
|
||||
<option key={`target-${person._id}`} value={person._id}>
|
||||
{person.firstName} {person.lastName}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Relationship Type" id="type" required>
|
||||
<select
|
||||
id="type"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
value={formData.type}
|
||||
onChange={e =>
|
||||
setFormData({
|
||||
...formData,
|
||||
type: e.target.value as RelationshipType,
|
||||
})
|
||||
}
|
||||
>
|
||||
{Object.entries(relationshipLabels).map(([value, label]) => (
|
||||
<option key={value} value={value}>
|
||||
{label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</FormField>
|
||||
|
||||
{formData.type === 'custom' && (
|
||||
<FormField label="Custom Type" id="customType" required error={errors.customType}>
|
||||
<input
|
||||
id="customType"
|
||||
type="text"
|
||||
className={`w-full bg-slate-700 border ${errors.customType ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
placeholder="Enter custom relationship type"
|
||||
value={formData.customType}
|
||||
onChange={e =>
|
||||
setFormData({
|
||||
...formData,
|
||||
customType: e.target.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</FormField>
|
||||
)}
|
||||
|
||||
<FormField label="Notes (Optional)" id="relationNotes">
|
||||
<textarea
|
||||
id="relationNotes"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 min-h-[60px]
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
placeholder="Add any additional information"
|
||||
value={formData.notes}
|
||||
onChange={e => setFormData({ ...formData, notes: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="flex items-center mt-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="bidirectional"
|
||||
className="h-4 w-4 rounded border-gray-500 text-indigo-600 focus:ring-indigo-500 bg-slate-700"
|
||||
checked={formData.bidirectional}
|
||||
onChange={e =>
|
||||
setFormData({
|
||||
...formData,
|
||||
bidirectional: e.target.checked,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<label htmlFor="bidirectional" className="ml-2 block text-sm text-gray-300">
|
||||
Create bidirectional relationship (recommended)
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end space-x-2 pt-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" icon={<FaUserFriends />}>
|
||||
Add Relationship
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// ==============================
|
||||
// Person Detail Modal
|
||||
// ==============================
|
||||
interface PersonDetailModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
person: PersonNode;
|
||||
setPerson: React.Dispatch<React.SetStateAction<PersonNode | null>>;
|
||||
errors: FormErrors;
|
||||
onSubmit: (e: React.FormEvent) => void;
|
||||
onDelete: (id: string) => void;
|
||||
relationships: RelationshipEdge[];
|
||||
people: PersonNode[];
|
||||
relationshipColors: Record<RelationshipType, string>;
|
||||
relationshipLabels: Record<RelationshipType, string>;
|
||||
onDeleteRelationship: (id: string) => void;
|
||||
onAddNewConnection: () => void;
|
||||
onNavigateToPerson: (id: string) => void;
|
||||
}
|
||||
|
||||
export const PersonDetailModal: React.FC<PersonDetailModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
person,
|
||||
setPerson,
|
||||
errors,
|
||||
onSubmit,
|
||||
onDelete,
|
||||
relationships,
|
||||
people,
|
||||
relationshipColors,
|
||||
relationshipLabels,
|
||||
onDeleteRelationship,
|
||||
onAddNewConnection,
|
||||
onNavigateToPerson,
|
||||
}) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title={`${person.firstName} ${person.lastName}`}>
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-4">
|
||||
<form onSubmit={onSubmit} className="space-y-4">
|
||||
<ErrorMessage message={errors.general} />
|
||||
|
||||
<FormField label="First Name" id="editFirstName" required error={errors.firstName}>
|
||||
<input
|
||||
id="editFirstName"
|
||||
type="text"
|
||||
className={`w-full bg-slate-700 border ${errors.firstName ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
value={person.firstName || ''}
|
||||
onChange={e => setPerson({ ...person, firstName: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Last Name" id="editLastName" required error={errors.lastName}>
|
||||
<input
|
||||
id="editLastName"
|
||||
type="text"
|
||||
className={`w-full bg-slate-700 border ${errors.lastName ? 'border-red-500' : 'border-slate-600'}
|
||||
rounded-md p-2 focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white`}
|
||||
value={person.lastName || ''}
|
||||
onChange={e => setPerson({ ...person, lastName: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Birthday" id="editBirthday">
|
||||
<div className="relative">
|
||||
<DatePicker
|
||||
id="editBirthday"
|
||||
selected={person.birthday ? new Date(person.birthday) : null}
|
||||
onChange={(date: Date | null) => setPerson({ ...person, birthday: date })}
|
||||
dateFormat="MMMM d, yyyy"
|
||||
placeholderText="Select birthday"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
showYearDropdown
|
||||
dropdownMode="select"
|
||||
wrapperClassName="w-full"
|
||||
/>
|
||||
<FaRegCalendarAlt className="absolute right-3 top-1/2 transform -translate-y-1/2 text-slate-400" />
|
||||
</div>
|
||||
</FormField>
|
||||
|
||||
<FormField label="Notes" id="editNotes">
|
||||
<textarea
|
||||
id="editNotes"
|
||||
className="w-full bg-slate-700 border border-slate-600 rounded-md p-2 min-h-[80px]
|
||||
focus:outline-none focus:ring-2 focus:ring-indigo-500 text-white"
|
||||
value={person.notes || ''}
|
||||
onChange={e => setPerson({ ...person, notes: e.target.value })}
|
||||
/>
|
||||
</FormField>
|
||||
|
||||
<div className="flex justify-between pt-2">
|
||||
<Button variant="danger" onClick={() => onDelete(person._id)} icon={<FaTrash />}>
|
||||
Delete
|
||||
</Button>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Button variant="secondary" onClick={onClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button type="submit" variant="primary" icon={<FaSave />}>
|
||||
Save Changes
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-medium text-indigo-400 mb-2">Connections</h4>
|
||||
<div className="max-h-40 overflow-y-auto space-y-1 bg-slate-900 rounded-lg p-2">
|
||||
{relationships.filter(
|
||||
(r: RelationshipEdge) => r.source === person._id || r.target === person._id
|
||||
).length > 0 ? (
|
||||
relationships
|
||||
.filter((r: RelationshipEdge) => r.source === person._id || r.target === person._id)
|
||||
.map((rel: RelationshipEdge) => {
|
||||
const isSource = rel.source === person._id;
|
||||
const otherPersonId = isSource ? rel.target : rel.source;
|
||||
const otherPerson = people.find((p: PersonNode) => p._id === otherPersonId);
|
||||
|
||||
if (!otherPerson) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={rel._id}
|
||||
className="flex justify-between items-center py-1 px-2 hover:bg-slate-800 rounded"
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<span
|
||||
className="inline-block w-2 h-2 rounded-full mr-2"
|
||||
style={{ backgroundColor: relationshipColors[rel.type] }}
|
||||
></span>
|
||||
<span className="text-sm">
|
||||
{isSource ? 'To: ' : 'From: '}
|
||||
<span
|
||||
className="font-medium hover:text-indigo-400 cursor-pointer"
|
||||
onClick={() => onNavigateToPerson(otherPersonId)}
|
||||
>
|
||||
{otherPerson.firstName} {otherPerson.lastName}
|
||||
</span>
|
||||
{rel.type === 'custom'
|
||||
? ` (${rel.customType})`
|
||||
: ` (${relationshipLabels[rel.type]})`}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
className="text-red-400 hover:text-red-300 transition-colors"
|
||||
onClick={() => onDeleteRelationship(rel._id)}
|
||||
>
|
||||
<FaTrash size={12} />
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<div className="text-center py-2 text-slate-400 text-sm">No connections yet</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 flex justify-center">
|
||||
<Button variant="secondary" size="sm" onClick={onAddNewConnection} icon={<FaPlus />}>
|
||||
Add New Connection
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// ==============================
|
||||
// Settings Modal
|
||||
// ==============================
|
||||
interface SettingsModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
settings: {
|
||||
darkMode: boolean;
|
||||
autoLayout: boolean;
|
||||
showLabels: boolean;
|
||||
animationSpeed: string;
|
||||
highlightConnections: boolean;
|
||||
nodeSize: string;
|
||||
};
|
||||
setSettings: React.Dispatch<
|
||||
React.SetStateAction<{
|
||||
darkMode: boolean;
|
||||
autoLayout: boolean;
|
||||
showLabels: boolean;
|
||||
animationSpeed: string;
|
||||
highlightConnections: boolean;
|
||||
nodeSize: string;
|
||||
}>
|
||||
>;
|
||||
}
|
||||
|
||||
export const SettingsModal: React.FC<SettingsModalProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
settings,
|
||||
setSettings,
|
||||
}) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Network Settings">
|
||||
<div className="space-y-4">
|
||||
{/* Toggle settings */}
|
||||
<ToggleSetting
|
||||
label="Show Labels"
|
||||
id="showLabels"
|
||||
checked={settings.showLabels}
|
||||
onChange={() => setSettings({ ...settings, showLabels: !settings.showLabels })}
|
||||
/>
|
||||
|
||||
<ToggleSetting
|
||||
label="Auto Layout"
|
||||
id="autoLayout"
|
||||
checked={settings.autoLayout}
|
||||
onChange={() => setSettings({ ...settings, autoLayout: !settings.autoLayout })}
|
||||
/>
|
||||
|
||||
<ToggleSetting
|
||||
label="Highlight Connections"
|
||||
id="highlightConnections"
|
||||
checked={settings.highlightConnections}
|
||||
onChange={() =>
|
||||
setSettings({
|
||||
...settings,
|
||||
highlightConnections: !settings.highlightConnections,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Option groups */}
|
||||
<OptionGroup
|
||||
label="Animation Speed"
|
||||
options={['slow', 'medium', 'fast']}
|
||||
currentValue={settings.animationSpeed}
|
||||
onChange={value => setSettings({ ...settings, animationSpeed: value })}
|
||||
/>
|
||||
|
||||
<OptionGroup
|
||||
label="Node Size"
|
||||
options={['small', 'medium', 'large']}
|
||||
currentValue={settings.nodeSize}
|
||||
onChange={value => setSettings({ ...settings, nodeSize: value })}
|
||||
/>
|
||||
|
||||
<div className="pt-4 flex justify-end">
|
||||
<Button variant="primary" onClick={onClose} icon={<FaSave />}>
|
||||
Save Settings
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
// ==============================
|
||||
// Help Modal
|
||||
// ==============================
|
||||
interface HelpModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const HelpModal: React.FC<HelpModalProps> = ({ isOpen, onClose }) => {
|
||||
return (
|
||||
<Modal isOpen={isOpen} onClose={onClose} title="Keyboard Shortcuts & Help" size="lg">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h3 className="text-md font-semibold text-indigo-400 mb-2">Keyboard Shortcuts</h3>
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
<KeyboardShortcut shortcut="n" description="Add new person" />
|
||||
<KeyboardShortcut shortcut="r" description="Add new relationship" />
|
||||
<KeyboardShortcut shortcut="s" description="Toggle sidebar" />
|
||||
<KeyboardShortcut shortcut="+" description="Zoom in" />
|
||||
<KeyboardShortcut shortcut="-" description="Zoom out" />
|
||||
<KeyboardShortcut shortcut="0" description="Reset zoom" />
|
||||
<KeyboardShortcut shortcut="Ctrl+/" description="Show this help" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 className="text-md font-semibold text-indigo-400 mb-2">Tips & Tricks</h3>
|
||||
<ul className="space-y-2 text-sm text-slate-300">
|
||||
<TipItem text="Click on a person in the graph to see their details and edit their information" />
|
||||
<TipItem text="Drag people around in the graph to organize your network visually" />
|
||||
<TipItem text="Use the sidebar to filter and manage your network's people and relationships" />
|
||||
<TipItem text="Create bidirectional relationships to show mutual connections (recommended)" />
|
||||
<TipItem text="Customize the appearance and behavior in Settings" />
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="text-center pt-2">
|
||||
<Button variant="primary" onClick={onClose} icon={<FaStar />}>
|
||||
Got it
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user