| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276 |
- import { useState, useEffect, useRef } from 'react';
- import { useMutation, useQueryClient, useQuery } from '@tanstack/react-query';
- import { X, Save, Loader2, RotateCcw, Plus, Eye } from 'lucide-react';
- import { api } from '../api/client';
- import type { NotificationTemplate, NotificationTemplateUpdate } from '../api/client';
- import { Button } from './Button';
- interface NotificationTemplateEditorProps {
- template: NotificationTemplate;
- onClose: () => void;
- }
- export function NotificationTemplateEditor({ template, onClose }: NotificationTemplateEditorProps) {
- const queryClient = useQueryClient();
- const bodyRef = useRef<HTMLTextAreaElement>(null);
- const [titleTemplate, setTitleTemplate] = useState(template.title_template);
- const [bodyTemplate, setBodyTemplate] = useState(template.body_template);
- const [error, setError] = useState<string | null>(null);
- const [showPreview, setShowPreview] = useState(true);
- // Fetch variables for this event type
- const { data: variablesData } = useQuery({
- queryKey: ['template-variables'],
- queryFn: api.getTemplateVariables,
- });
- // Get variables for this template's event type
- const eventVariables = variablesData?.find(v => v.event_type === template.event_type);
- // Live preview
- const { data: preview, isLoading: previewLoading } = useQuery({
- queryKey: ['template-preview', template.event_type, titleTemplate, bodyTemplate],
- queryFn: () => api.previewTemplate({
- event_type: template.event_type,
- title_template: titleTemplate,
- body_template: bodyTemplate,
- }),
- enabled: showPreview && titleTemplate.length > 0 && bodyTemplate.length > 0,
- });
- // Close on Escape key
- useEffect(() => {
- const handleKeyDown = (e: KeyboardEvent) => {
- if (e.key === 'Escape') onClose();
- };
- window.addEventListener('keydown', handleKeyDown);
- return () => window.removeEventListener('keydown', handleKeyDown);
- }, [onClose]);
- // Update mutation
- const updateMutation = useMutation({
- mutationFn: (data: NotificationTemplateUpdate) => api.updateNotificationTemplate(template.id, data),
- onSuccess: () => {
- queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
- onClose();
- },
- onError: (err: Error) => {
- setError(err.message);
- },
- });
- // Reset mutation
- const resetMutation = useMutation({
- mutationFn: () => api.resetNotificationTemplate(template.id),
- onSuccess: (resetTemplate) => {
- setTitleTemplate(resetTemplate.title_template);
- setBodyTemplate(resetTemplate.body_template);
- queryClient.invalidateQueries({ queryKey: ['notification-templates'] });
- },
- onError: (err: Error) => {
- setError(err.message);
- },
- });
- const handleSubmit = (e: React.FormEvent) => {
- e.preventDefault();
- setError(null);
- if (!titleTemplate.trim()) {
- setError('Title is required');
- return;
- }
- if (!bodyTemplate.trim()) {
- setError('Body is required');
- return;
- }
- updateMutation.mutate({
- title_template: titleTemplate,
- body_template: bodyTemplate,
- });
- };
- const insertVariable = (variable: string) => {
- const textarea = bodyRef.current;
- if (!textarea) return;
- const start = textarea.selectionStart;
- const end = textarea.selectionEnd;
- const text = bodyTemplate;
- const before = text.substring(0, start);
- const after = text.substring(end);
- const newValue = before + `{${variable}}` + after;
- setBodyTemplate(newValue);
- // Restore focus and cursor position
- setTimeout(() => {
- textarea.focus();
- const newCursor = start + variable.length + 2;
- textarea.setSelectionRange(newCursor, newCursor);
- }, 0);
- };
- const hasChanges = titleTemplate !== template.title_template || bodyTemplate !== template.body_template;
- return (
- <div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
- <div className="bg-bambu-dark-secondary rounded-lg w-full max-w-2xl max-h-[90vh] flex flex-col">
- {/* Header */}
- <div className="flex items-center justify-between p-4 border-b border-bambu-dark-tertiary shrink-0">
- <h2 className="text-lg font-semibold text-white">
- Edit Template: {template.name}
- </h2>
- <button
- onClick={onClose}
- className="p-1 hover:bg-bambu-dark-tertiary rounded transition-colors"
- >
- <X className="w-5 h-5 text-bambu-gray" />
- </button>
- </div>
- {/* Content */}
- <form onSubmit={handleSubmit} className="flex-1 overflow-y-auto p-4 space-y-4">
- {error && (
- <div className="p-3 bg-red-500/20 border border-red-500/50 rounded text-red-400 text-sm">
- {error}
- </div>
- )}
- {/* Title */}
- <div>
- <label className="block text-sm font-medium text-bambu-gray mb-1">
- Title
- </label>
- <input
- type="text"
- value={titleTemplate}
- onChange={(e) => setTitleTemplate(e.target.value)}
- className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green"
- placeholder="Notification title..."
- />
- </div>
- {/* Body */}
- <div>
- <label className="block text-sm font-medium text-bambu-gray mb-1">
- Body
- </label>
- <textarea
- ref={bodyRef}
- value={bodyTemplate}
- onChange={(e) => setBodyTemplate(e.target.value)}
- rows={4}
- className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded text-white focus:outline-none focus:ring-1 focus:ring-bambu-green font-mono text-sm resize-none"
- placeholder="Notification body..."
- />
- </div>
- {/* Available Variables */}
- {eventVariables && (
- <div>
- <label className="block text-sm font-medium text-bambu-gray mb-2">
- Available Variables
- </label>
- <div className="flex flex-wrap gap-2">
- {eventVariables.variables.map((variable) => (
- <button
- key={variable}
- type="button"
- onClick={() => insertVariable(variable)}
- className="inline-flex items-center gap-1 px-2 py-1 bg-bambu-dark hover:bg-bambu-dark-tertiary border border-bambu-dark-tertiary rounded text-xs text-bambu-gray hover:text-white transition-colors"
- >
- <Plus className="w-3 h-3" />
- {variable}
- </button>
- ))}
- </div>
- <p className="text-xs text-bambu-gray/60 mt-1">
- Click to insert at cursor position in body
- </p>
- </div>
- )}
- {/* Preview */}
- <div>
- <div className="flex items-center justify-between mb-2">
- <label className="text-sm font-medium text-bambu-gray flex items-center gap-2">
- <Eye className="w-4 h-4" />
- Live Preview
- </label>
- <button
- type="button"
- onClick={() => setShowPreview(!showPreview)}
- className="text-xs text-bambu-green hover:text-bambu-green-light"
- >
- {showPreview ? 'Hide' : 'Show'}
- </button>
- </div>
- {showPreview && (
- <div className="bg-bambu-dark border border-bambu-dark-tertiary rounded p-3 space-y-2">
- {previewLoading ? (
- <div className="flex items-center gap-2 text-bambu-gray text-sm">
- <Loader2 className="w-4 h-4 animate-spin" />
- Loading preview...
- </div>
- ) : preview ? (
- <>
- <div>
- <span className="text-xs text-bambu-gray">Title:</span>
- <div className="text-white font-medium">{preview.title}</div>
- </div>
- <div>
- <span className="text-xs text-bambu-gray">Body:</span>
- <div className="text-white whitespace-pre-wrap text-sm">{preview.body}</div>
- </div>
- </>
- ) : (
- <div className="text-bambu-gray text-sm">
- Enter template content to see preview
- </div>
- )}
- </div>
- )}
- </div>
- </form>
- {/* Footer */}
- <div className="flex items-center justify-between p-4 border-t border-bambu-dark-tertiary shrink-0">
- <Button
- type="button"
- variant="ghost"
- onClick={() => resetMutation.mutate()}
- disabled={resetMutation.isPending}
- className="text-orange-400 hover:text-orange-300"
- >
- {resetMutation.isPending ? (
- <Loader2 className="w-4 h-4 animate-spin mr-2" />
- ) : (
- <RotateCcw className="w-4 h-4 mr-2" />
- )}
- Reset to Default
- </Button>
- <div className="flex gap-2">
- <Button type="button" variant="secondary" onClick={onClose}>
- Cancel
- </Button>
- <Button
- onClick={handleSubmit}
- disabled={updateMutation.isPending || !hasChanges}
- >
- {updateMutation.isPending ? (
- <Loader2 className="w-4 h-4 animate-spin mr-2" />
- ) : (
- <Save className="w-4 h-4 mr-2" />
- )}
- Save
- </Button>
- </div>
- </div>
- </div>
- </div>
- );
- }
|