Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
231 changes: 231 additions & 0 deletions app/feedback/FeedbackForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
'use client';

import { useState } from 'react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import { Frown, Meh, Smile, Laugh, Heart } from 'lucide-react';

export default function FeedbackForm({ isAuthenticated }: { isAuthenticated: boolean }) {
const router = useRouter();
const [rating, setRating] = useState<number | null>(null);
const [comments, setComments] = useState('');
const [challenges, setChallenges] = useState('');
const [improvements, setImprovements] = useState('');
const [malfunctions, setMalfunctions] = useState('');
const [attachmentUrl, setAttachmentUrl] = useState('');
const [anonymousName, setAnonymousName] = useState('');
const [anonymousEmail, setAnonymousEmail] = useState('');
const [anonymousMatric, setAnonymousMatric] = useState('');
const [submitting, setSubmitting] = useState(false);

const ratingConfig = [
{ value: 1, icon: Frown, label: 'Poor', color: '#ef4444' },
{ value: 2, icon: Meh, label: 'Fair', color: '#f97316' },
{ value: 3, icon: Smile, label: 'Good', color: '#eab308' },
{ value: 4, icon: Laugh, label: 'Great', color: '#84cc16' },
{ value: 5, icon: Heart, label: 'Excellent', color: '#10b981' },
];

async function handleSubmit(e: React.FormEvent) {
e.preventDefault();

// Validate name for anonymous users
if (!isAuthenticated && !anonymousName.trim()) {
toast.error('Please provide your name');
return;
}

setSubmitting(true);

try {
const res = await fetch('/api/feedback', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
rating,
comments,
challenges,
improvements,
malfunctions,
attachment_url: attachmentUrl || null,
anonymous_name: !isAuthenticated ? anonymousName : undefined,
anonymous_email: !isAuthenticated ? anonymousEmail : undefined,
anonymous_matric: !isAuthenticated ? anonymousMatric : undefined,
}),
});

if (!res.ok) {
const data = await res.json();
throw new Error(data.error ?? 'Failed to submit feedback');
}

toast.success('Thank you for your feedback!');
router.push('/');
} catch (err) {
toast.error(err instanceof Error ? err.message : 'Failed to submit');
} finally {
setSubmitting(false);
}
}

return (
<form onSubmit={handleSubmit} style={{ display: 'flex', flexDirection: 'column', gap: '1.5rem' }}>
{/* Anonymous User Info */}
{!isAuthenticated && (
<div className="card">
<div className="card-header">
<span className="card-title">Your Information</span>
<span className="badge badge-red">Required</span>
</div>
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
<input
type="text"
className="form-input"
value={anonymousName}
onChange={(e) => setAnonymousName(e.target.value)}
placeholder="Name / Username *"
required
style={{ border: 'none', background: 'var(--bg2)' }}
/>
<input
type="email"
className="form-input"
value={anonymousEmail}
onChange={(e) => setAnonymousEmail(e.target.value)}
placeholder="Email (optional)"
style={{ border: 'none', background: 'var(--bg2)' }}
/>
<input
type="text"
className="form-input"
value={anonymousMatric}
onChange={(e) => setAnonymousMatric(e.target.value)}
placeholder="Matric Number (optional)"
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>
</div>
)}

{/* Rating */}
<div className="card">
<div className="card-header">
<span className="card-title">Overall Experience</span>
</div>
<div style={{ display: 'flex', gap: '1rem', justifyContent: 'center', padding: '1.5rem 0' }}>
{ratingConfig.map(({ value, icon: Icon, label, color }) => (
<button
key={value}
type="button"
onClick={() => setRating(value)}
className="btn"
style={{
width: 80,
height: 80,
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
gap: '0.25rem',
background: rating === value ? `${color}15` : 'var(--bg3)',
border: rating === value ? `2px solid ${color}` : '1px solid var(--border)',
color: rating === value ? color : 'var(--text3)',
transition: 'all 0.2s',
}}
>
<Icon size={28} strokeWidth={2} />
<span style={{ fontSize: 11, fontWeight: 600 }}>{label}</span>
</button>
))}
</div>
</div>

{/* Challenges */}
<div className="card">
<div className="card-header">
<span className="card-title">Challenges Encountered</span>
</div>
<textarea
className="form-textarea"
rows={4}
value={challenges}
onChange={(e) => setChallenges(e.target.value)}
placeholder="Describe any difficulties you faced during the exercise..."
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>

{/* Malfunctions */}
<div className="card">
<div className="card-header">
<span className="card-title">Bugs & Technical Issues</span>
</div>
<textarea
className="form-textarea"
rows={4}
value={malfunctions}
onChange={(e) => setMalfunctions(e.target.value)}
placeholder="Report any errors, crashes, or unexpected behavior..."
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>

{/* Improvements */}
<div className="card">
<div className="card-header">
<span className="card-title">Suggested Improvements</span>
</div>
<textarea
className="form-textarea"
rows={4}
value={improvements}
onChange={(e) => setImprovements(e.target.value)}
placeholder="Share your ideas for making the platform better..."
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>

{/* Additional Comments */}
<div className="card">
<div className="card-header">
<span className="card-title">Additional Comments</span>
<span className="badge badge-gray">Optional</span>
</div>
<textarea
className="form-textarea"
rows={3}
value={comments}
onChange={(e) => setComments(e.target.value)}
placeholder="Any other feedback you'd like to share..."
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>

{/* Attachment */}
<div className="card">
<div className="card-header">
<span className="card-title">Supporting Document</span>
<span className="badge badge-gray">Optional</span>
</div>
<input
type="url"
className="form-input"
value={attachmentUrl}
onChange={(e) => setAttachmentUrl(e.target.value)}
placeholder="Paste a link to a screenshot or document (Google Drive, Imgur, etc.)"
style={{ border: 'none', background: 'var(--bg2)' }}
/>
</div>

{/* Submit */}
<div style={{ display: 'flex', gap: '0.75rem', justifyContent: 'center' }}>
<button type="submit" disabled={submitting} className="btn btn-primary btn-lg">
{submitting ? 'Submitting…' : 'Submit Feedback'}
</button>
<button type="button" onClick={() => router.push('/')} className="btn btn-ghost btn-lg">
Cancel
</button>
</div>
</form>
);
}
27 changes: 27 additions & 0 deletions app/feedback/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getServerSession } from 'next-auth';
import { authOptions } from '@/lib/auth';
import Navbar from '@/app/components/Navbar';
import FeedbackForm from './FeedbackForm';

export default async function FeedbackPage() {
const session = await getServerSession(authOptions);

return (
<div className="page">
<Navbar
username={session?.user?.name ?? undefined}
role={session?.user?.role === 'instructor' ? 'instructor' : 'participant'}
links={session?.user ? [{ href: session.user.role === 'instructor' ? '/instructor' : '/participant', label: session.user.role === 'instructor' ? 'Dashboard' : 'My Exercises' }] : []}
/>
<main className="main">
<div className="container" style={{ maxWidth: 800 }}>
<div className="page-header">
<h1 className="page-title">Feedback</h1>
<p className="page-sub">Help us improve the platform</p>
</div>
<FeedbackForm isAuthenticated={!!session?.user} />
</div>
</main>
</div>
);
}
Loading