mirror of
https://github.com/Oak-and-Sprout/sprout-track.git
synced 2026-05-07 07:29:24 -05:00
removed orphaned components
This commit is contained in:
@@ -1,128 +0,0 @@
|
||||
# FeedbackForm Component
|
||||
|
||||
A form component for collecting user feedback and support requests. This component follows the form-page pattern used throughout the application and automatically captures user context information.
|
||||
|
||||
## Features
|
||||
|
||||
- Collects feedback with subject and message
|
||||
- Automatically captures submitter information (name, email, family context)
|
||||
- Form validation for required fields
|
||||
- Responsive design with dark mode support
|
||||
- Multi-family support with family ID association
|
||||
- Success/error handling with user feedback
|
||||
|
||||
## Props
|
||||
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `isOpen` | boolean | Yes | Controls whether the form is visible |
|
||||
| `onClose` | () => void | Yes | Function to call when the form should be closed |
|
||||
| `onSuccess` | () => void | No | Optional callback function called after successful submission |
|
||||
|
||||
## Usage
|
||||
|
||||
```tsx
|
||||
import FeedbackForm from '@/src/components/forms/FeedbackForm';
|
||||
|
||||
function MyComponent() {
|
||||
const [showFeedbackForm, setShowFeedbackForm] = useState(false);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => setShowFeedbackForm(true)}>
|
||||
Send Feedback
|
||||
</Button>
|
||||
|
||||
<FeedbackForm
|
||||
isOpen={showFeedbackForm}
|
||||
onClose={() => setShowFeedbackForm(false)}
|
||||
onSuccess={() => {
|
||||
// Optional: Handle successful feedback submission
|
||||
console.log('Feedback submitted successfully');
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Form Fields
|
||||
|
||||
The component includes the following fields:
|
||||
|
||||
- **Subject**: Brief description of the feedback (required)
|
||||
- **Message**: Detailed feedback, suggestions, or issue report (required)
|
||||
|
||||
## Automatic Context Capture
|
||||
|
||||
The component automatically captures and displays:
|
||||
|
||||
- **Submitter Name**: Extracted from authentication token (account email prefix or caretaker name)
|
||||
- **Submitter Email**: Account email if available (account users only)
|
||||
- **Family Context**: Current family name and ID from family context
|
||||
|
||||
## Implementation Details
|
||||
|
||||
- Uses the FormPage component for consistent UI across the application
|
||||
- Automatically extracts user information from JWT authentication token
|
||||
- Supports both account-based and caretaker-based authentication
|
||||
- Includes form validation for required fields
|
||||
- Handles API calls for submitting feedback
|
||||
- Provides user feedback through alerts for success/error states
|
||||
- Resets form after successful submission
|
||||
- Uses emerald/green theme colors to indicate positive action (feedback submission)
|
||||
|
||||
### Authentication Context
|
||||
|
||||
The component handles different authentication scenarios:
|
||||
|
||||
1. **Account Authentication**: Extracts email and uses email prefix as name
|
||||
2. **Caretaker Authentication**: Extracts caretaker name from token
|
||||
3. **No Authentication**: Falls back to generic "User" name
|
||||
|
||||
### Multi-Family Support
|
||||
|
||||
The component supports multi-family functionality by:
|
||||
- Automatically using the current family context from `useFamily()` hook
|
||||
- Including the family ID in the API request payload
|
||||
- Displaying the current family name in the submitter information
|
||||
|
||||
### API Integration
|
||||
|
||||
The component sends feedback data to `/api/feedback` with the following payload:
|
||||
```json
|
||||
{
|
||||
"subject": "string",
|
||||
"message": "string",
|
||||
"familyId": "string|null",
|
||||
"submitterName": "string",
|
||||
"submitterEmail": "string|null"
|
||||
}
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
The component uses:
|
||||
- Light mode styles defined in the component and CSS classes
|
||||
- Dark mode overrides in `feedback-form.css`
|
||||
- Consistent styling with other form components
|
||||
- Emerald/green accent colors for the submit button
|
||||
- Info box styling for displaying submitter context
|
||||
|
||||
## Accessibility
|
||||
|
||||
- Proper form labels and required field indicators
|
||||
- Keyboard navigation support
|
||||
- Screen reader friendly structure
|
||||
- Clear error and success messaging
|
||||
- Disabled states during form submission
|
||||
|
||||
## Error Handling
|
||||
|
||||
The component handles various error scenarios:
|
||||
- Network errors during submission
|
||||
- API validation errors
|
||||
- Authentication token parsing errors
|
||||
- Missing required fields
|
||||
|
||||
All errors are displayed to the user through alert dialogs with descriptive messages.
|
||||
@@ -1,115 +0,0 @@
|
||||
/* Dark mode overrides for FeedbackForm component */
|
||||
|
||||
/* Submitter info section in dark mode */
|
||||
html.dark .feedback-form-submitter-info {
|
||||
color: #9ca3af !important; /* gray-400 */
|
||||
}
|
||||
|
||||
/* Info container styling in dark mode */
|
||||
html.dark .feedback-form-info {
|
||||
background-color: #374151 !important; /* gray-700 */
|
||||
border: 1px solid #4b5563 !important; /* gray-600 */
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
/* Input field styling in dark mode */
|
||||
html.dark .feedback-form-input {
|
||||
background-color: #1f2937 !important; /* gray-800 */
|
||||
border-color: #4b5563 !important; /* gray-600 */
|
||||
color: #e5e7eb !important; /* gray-200 */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-input:hover {
|
||||
background-color: #374151 !important; /* gray-700 */
|
||||
border-color: #6b7280 !important; /* gray-500 */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-input:focus {
|
||||
border-color: #10b981 !important; /* emerald-500 */
|
||||
background-color: #1f2937 !important; /* gray-800 */
|
||||
ring-color: rgba(16, 185, 129, 0.2) !important; /* emerald-500 with opacity */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-input::placeholder {
|
||||
color: #9ca3af !important; /* gray-400 */
|
||||
}
|
||||
|
||||
/* Textarea styling in dark mode */
|
||||
html.dark .feedback-form-textarea {
|
||||
background-color: #1f2937 !important; /* gray-800 */
|
||||
border-color: #4b5563 !important; /* gray-600 */
|
||||
color: #e5e7eb !important; /* gray-200 */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-textarea:hover {
|
||||
background-color: #374151 !important; /* gray-700 */
|
||||
border-color: #6b7280 !important; /* gray-500 */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-textarea:focus {
|
||||
border-color: #10b981 !important; /* emerald-500 */
|
||||
background-color: #1f2937 !important; /* gray-800 */
|
||||
ring-color: rgba(16, 185, 129, 0.2) !important; /* emerald-500 with opacity */
|
||||
}
|
||||
|
||||
html.dark .feedback-form-textarea::placeholder {
|
||||
color: #9ca3af !important; /* gray-400 */
|
||||
}
|
||||
|
||||
/* Help text styling in dark mode */
|
||||
html.dark .feedback-form-help-text {
|
||||
color: #9ca3af !important; /* gray-400 */
|
||||
}
|
||||
|
||||
/* Light mode styles for info container */
|
||||
.feedback-form-info {
|
||||
background-color: #f8fafc; /* slate-50 */
|
||||
border: 1px solid #e2e8f0; /* slate-200 */
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
}
|
||||
|
||||
.feedback-form-submitter-info {
|
||||
color: #64748b; /* slate-500 */
|
||||
}
|
||||
|
||||
.feedback-form-help-text {
|
||||
color: #64748b; /* slate-500 */
|
||||
}
|
||||
|
||||
/* Success toast styling */
|
||||
.feedback-success-toast {
|
||||
background-color: #ecfdf5; /* emerald-50 */
|
||||
border-color: #a7f3d0; /* emerald-200 */
|
||||
}
|
||||
|
||||
.feedback-success-toast-icon {
|
||||
color: #6ee7b7; /* emerald-300 */
|
||||
}
|
||||
|
||||
.feedback-success-toast-title {
|
||||
color: #065f46; /* emerald-800 */
|
||||
}
|
||||
|
||||
.feedback-success-toast-message {
|
||||
color: #047857; /* emerald-700 */
|
||||
}
|
||||
|
||||
/* Dark mode success toast styling */
|
||||
html.dark .feedback-success-toast {
|
||||
background-color: #064e3b !important; /* emerald-900 */
|
||||
border-color: #065f46 !important; /* emerald-800 */
|
||||
}
|
||||
|
||||
html.dark .feedback-success-toast-icon {
|
||||
color: #6ee7b7 !important; /* emerald-300 */
|
||||
}
|
||||
|
||||
html.dark .feedback-success-toast-title {
|
||||
color: #a7f3d0 !important; /* emerald-200 */
|
||||
}
|
||||
|
||||
html.dark .feedback-success-toast-message {
|
||||
color: #6ee7b7 !important; /* emerald-300 */
|
||||
}
|
||||
@@ -1,311 +0,0 @@
|
||||
'use client';
|
||||
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Button } from '@/src/components/ui/button';
|
||||
import { Input } from '@/src/components/ui/input';
|
||||
import { Textarea } from '@/src/components/ui/textarea';
|
||||
import {
|
||||
FormPage,
|
||||
FormPageContent,
|
||||
FormPageFooter
|
||||
} from '@/src/components/ui/form-page';
|
||||
import { useTheme } from '@/src/context/theme';import { useLocalization } from '@/src/context/localization';
|
||||
|
||||
import './feedback-form.css';
|
||||
|
||||
interface FeedbackFormProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSuccess?: () => void;
|
||||
embedded?: boolean; // If true, renders without FormPage wrapper
|
||||
}
|
||||
|
||||
export default function FeedbackForm({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSuccess,
|
||||
embedded = false,
|
||||
}: FeedbackFormProps) {
|
||||
const { t } = useLocalization();
|
||||
const { theme } = useTheme();
|
||||
const [formData, setFormData] = useState({
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [submitterInfo, setSubmitterInfo] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
});
|
||||
const [family, setFamily] = useState<{ id: string; name: string } | null>(null);
|
||||
const [showSuccessToast, setShowSuccessToast] = useState(false);
|
||||
|
||||
// Get submitter information from authentication context
|
||||
useEffect(() => {
|
||||
const getSubmitterInfo = async () => {
|
||||
try {
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
if (!authToken) return;
|
||||
|
||||
// Parse JWT token to get user info
|
||||
const payload = authToken.split('.')[1];
|
||||
const decodedPayload = JSON.parse(atob(payload));
|
||||
|
||||
// Check if this is account authentication
|
||||
if (decodedPayload.isAccountAuth) {
|
||||
// For account users, we can get email from the token
|
||||
setSubmitterInfo({
|
||||
name: decodedPayload.accountEmail ? decodedPayload.accountEmail.split('@')[0] : 'Account User',
|
||||
email: decodedPayload.accountEmail || '',
|
||||
});
|
||||
} else {
|
||||
// For caretaker users, get name from token
|
||||
setSubmitterInfo({
|
||||
name: decodedPayload.name || 'User',
|
||||
email: '', // Caretakers don't have email in the token
|
||||
});
|
||||
}
|
||||
|
||||
// Try to get family information if available
|
||||
if (decodedPayload.familyId && decodedPayload.familySlug) {
|
||||
// We have family info in the token, try to get family name
|
||||
try {
|
||||
const familyResponse = await fetch(`/api/family/by-slug/${decodedPayload.familySlug}`, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${authToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (familyResponse.ok) {
|
||||
const familyData = await familyResponse.json();
|
||||
if (familyData.success && familyData.data) {
|
||||
setFamily({
|
||||
id: familyData.data.id,
|
||||
name: familyData.data.name
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (familyError) {
|
||||
console.log('Could not fetch family info:', familyError);
|
||||
// Not a critical error, continue without family info
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error parsing auth token:', error);
|
||||
setSubmitterInfo({
|
||||
name: 'User',
|
||||
email: '',
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
if (isOpen) {
|
||||
getSubmitterInfo();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
// Reset form when opening
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setFormData({
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData(prev => ({ ...prev, [name]: value }));
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
// Validate required fields
|
||||
if (!formData.subject.trim() || !formData.message.trim()) {
|
||||
alert(t('Please fill in both subject and message fields.'));
|
||||
return;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
|
||||
try {
|
||||
const authToken = localStorage.getItem('authToken');
|
||||
|
||||
const payload = {
|
||||
subject: formData.subject.trim(),
|
||||
message: formData.message.trim(),
|
||||
familyId: family?.id || null,
|
||||
submitterName: submitterInfo.name,
|
||||
submitterEmail: submitterInfo.email || null,
|
||||
};
|
||||
|
||||
const response = await fetch('/api/feedback', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': authToken ? `Bearer ${authToken}` : '',
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
// Show success toast
|
||||
setShowSuccessToast(true);
|
||||
|
||||
// Auto-close after 3 seconds
|
||||
setTimeout(() => {
|
||||
onClose();
|
||||
if (onSuccess) onSuccess();
|
||||
}, 3000);
|
||||
} else {
|
||||
console.error('Error submitting feedback:', data.error);
|
||||
alert(`Error: ${data.error || 'Failed to submit feedback'}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error submitting feedback:', error);
|
||||
alert(t('An unexpected error occurred. Please try again.'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const formContent = (
|
||||
<>
|
||||
{/* Success Toast */}
|
||||
{showSuccessToast && (
|
||||
<div className="mb-4 p-4 rounded-lg feedback-success-toast">
|
||||
<div className="flex items-center">
|
||||
<div className="flex-shrink-0">
|
||||
<svg className="h-5 w-5 feedback-success-toast-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clipRule="evenodd" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="ml-3">
|
||||
<p className="text-sm font-medium feedback-success-toast-title">
|
||||
{t('Thank you for your feedback!')}
|
||||
</p>
|
||||
<p className="text-sm mt-1 feedback-success-toast-message">
|
||||
{t('We appreciate your input and will review your message.')}
|
||||
{submitterInfo.email && ' A confirmation email has been sent to you.'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="space-y-4">
|
||||
{/* Submitter Info Display */}
|
||||
<div className="feedback-form-info">
|
||||
<div className="text-sm text-gray-600 feedback-form-submitter-info">
|
||||
<p><strong>{t('From:')}</strong> {submitterInfo.name}</p>
|
||||
{submitterInfo.email && (
|
||||
<p><strong>{t('Email:')}</strong> {submitterInfo.email}</p>
|
||||
)}
|
||||
{family && (
|
||||
<p><strong>{t('Family:')}</strong> {family.name}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subject */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="subject" className="form-label">
|
||||
{t('Subject')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Input
|
||||
id="subject"
|
||||
name="subject"
|
||||
type="text"
|
||||
placeholder={t("Brief description of your feedback")}
|
||||
value={formData.subject}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={loading}
|
||||
className="feedback-form-input"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Message */}
|
||||
<div className="space-y-2">
|
||||
<label htmlFor="message" className="form-label">
|
||||
{t('Message')} <span className="text-red-500">*</span>
|
||||
</label>
|
||||
<Textarea
|
||||
id="message"
|
||||
name="message"
|
||||
placeholder={t("Please share your detailed feedback, suggestions, or report any issues you've encountered...")}
|
||||
value={formData.message}
|
||||
onChange={handleInputChange}
|
||||
required
|
||||
disabled={loading}
|
||||
rows={6}
|
||||
className="feedback-form-textarea"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Help Text */}
|
||||
<div className="feedback-form-help-text">
|
||||
<p className="text-sm text-gray-500">
|
||||
{t('Your feedback helps us improve the app. Please be as specific as possible about any issues or suggestions you have.')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</>
|
||||
);
|
||||
|
||||
const formFooter = (
|
||||
<div className="flex justify-end space-x-2">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
disabled={loading}
|
||||
>
|
||||
{t('Cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading || !formData.subject.trim() || !formData.message.trim()}
|
||||
variant="success"
|
||||
>
|
||||
{loading ? t('Sending...') : t('Send Feedback')}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (embedded) {
|
||||
return (
|
||||
<>
|
||||
<FormPageContent>
|
||||
{formContent}
|
||||
</FormPageContent>
|
||||
<FormPageFooter>
|
||||
{formFooter}
|
||||
</FormPageFooter>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<FormPage
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
title={t("Send Feedback")}
|
||||
description={t("Help us improve by sharing your thoughts and suggestions")}
|
||||
>
|
||||
<FormPageContent>
|
||||
{formContent}
|
||||
</FormPageContent>
|
||||
|
||||
<FormPageFooter>
|
||||
{formFooter}
|
||||
</FormPageFooter>
|
||||
</FormPage>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user