Skip to content

Commit 6bf8921

Browse files
Merge pull request #20 from CodeCompasss/complaient
Complaient
2 parents b6e2e2d + 5fa447e commit 6bf8921

File tree

6 files changed

+315
-8
lines changed

6 files changed

+315
-8
lines changed

src/app/complaint/new/page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
'use client';
2+
3+
import ComplaintForm from '@/components/complaint/ComplaintForm';
4+
import { useRouter } from 'next/navigation';
5+
6+
export default function NewComplaintPage() {
7+
const router = useRouter();
8+
9+
return (
10+
<div className="min-h-screen bg-gray-100 flex flex-col relative">
11+
{/* Scrollable Content with Bottom Padding */}
12+
<div className="overflow-y-auto flex-1 p-4 pb-36">
13+
14+
15+
16+
{/* Title */}
17+
<div className="mt-12 mb-6 text-center">
18+
<h1 className="text-2xl font-bold">Complaint</h1>
19+
<h2 className="text-lg font-medium text-gray-600 mt-2">Add Details</h2>
20+
</div>
21+
22+
{/* Back Button */}
23+
<button
24+
onClick={() => router.back()}
25+
className="text-xl absolute top-4 left-4 bg-white p-2 rounded-full shadow-md z-10"
26+
aria-label="Go Back"
27+
>
28+
29+
</button>
30+
31+
{/* Complaint Form */}
32+
<ComplaintForm />
33+
</div>
34+
</div>
35+
);
36+
}

src/app/complaint/page.tsx

Lines changed: 68 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,72 @@
1-
export default function Home() {
1+
'use client';
2+
3+
import { useState, useEffect } from 'react';
4+
import ComplaintCard from '@/components/complaint/Complaintcard';
5+
import Tabs from '@/components/complaint/Tabs';
6+
import Link from 'next/link';
7+
import { useRouter } from 'next/navigation';
8+
9+
type Complaint = {
10+
id: number;
11+
title: string;
12+
description: string;
13+
place: string;
14+
date: string;
15+
solved: boolean;
16+
imageUrl?: string;
17+
comments?: { id: number; text: string; timestamp: string }[];
18+
};
19+
20+
export default function ComplaintPage() {
21+
const router = useRouter();
22+
const [selectedTab, setSelectedTab] = useState<'solved' | 'unsolved'>('unsolved');
23+
const [complaints, setComplaints] = useState<Complaint[]>([]);
24+
25+
/*useEffect(() => {
26+
async function fetchComplaints() {
27+
try {
28+
const response = await fetch('/api/complaints');
29+
if (!response.ok) {
30+
throw new Error();
31+
}
32+
const data = await response.json();
33+
setComplaints(data);
34+
} catch (err) {
35+
console.error('Error fetching complaints:', err);
36+
}
37+
}
38+
39+
fetchComplaints();
40+
}, [])*/
41+
242
return (
3-
<div>
4-
<p>
5-
the most importnetn thing is it need to be annoynmous but the suer who posted it should be able to delete or obivoulsy admin need to able to dlete it as well
6-
it need stackoverflow like each complaint need to able to upvote or downvote
7-
also filter latest popular etc
8-
</p>
43+
<div className="min-h-screen p-4 pb-28 bg-gray-100">
44+
<h1 className="text-2xl font-bold text-center mb-4">Complaint</h1>
45+
46+
<Tabs selected={selectedTab} onChange={setSelectedTab} />
47+
48+
{complaints
49+
.filter((c) => c.solved === (selectedTab === 'solved'))
50+
.map((c) => (
51+
<ComplaintCard key={c.id} complaint={c} />
52+
))}
53+
54+
<Link href="/complaint/new">
55+
<button
56+
className="fixed bottom-24 right-4 bg-blue-500 hover:bg-blue-600 text-white p-4 rounded-full text-2xl shadow-lg"
57+
aria-label="Add new complaint"
58+
>
59+
+
60+
</button>
61+
</Link>
62+
63+
<button
64+
onClick={() => router.back()}
65+
className="text-xl absolute top-32 left-4 bg-white p-2 rounded-full shadow-md z-10"
66+
aria-label="Go Back"
67+
>
68+
69+
</button>
970
</div>
1071
);
1172
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
'use client';
2+
import { useState, ChangeEvent, FormEvent } from 'react';
3+
4+
export default function ComplaintForm() {
5+
const [title, setTitle] = useState('');
6+
const [description, setDescription] = useState('');
7+
const [place, setPlace] = useState('');
8+
const [image, setImage] = useState<File | null>(null);
9+
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
10+
11+
const handleImageUpload = (e: ChangeEvent<HTMLInputElement>) => {
12+
const file = e.target.files?.[0];
13+
if (file) {
14+
setImage(file);
15+
setPreviewUrl(URL.createObjectURL(file));
16+
}
17+
};
18+
19+
const toBase64 = (file: File): Promise<string> => {
20+
return new Promise((resolve, reject) => {
21+
const reader = new FileReader();
22+
reader.readAsDataURL(file);
23+
reader.onload = () => resolve(reader.result as string);
24+
reader.onerror = (error) => reject(error);
25+
});
26+
};
27+
28+
const handleSubmit = async (e: FormEvent) => {
29+
e.preventDefault();
30+
31+
if (!title.trim() || !description.trim() || !place.trim() || !image) {
32+
alert('Please fill in all fields and upload an image.');
33+
return;
34+
}
35+
36+
let imageUrl = null;
37+
try {
38+
imageUrl = await toBase64(image);
39+
} catch (err) {
40+
console.error('Error converting image:', err);
41+
alert('Failed to convert image.');
42+
return;
43+
}
44+
45+
// ⛔ Don't actually send to API now
46+
const formData = { title, description, place, imageUrl };
47+
console.log('📝 Collected Complaint Data:', formData);
48+
49+
alert('Form submitted (not sent to backend). Check console for data.');
50+
51+
// Reset fields (optional)
52+
setTitle('');
53+
setDescription('');
54+
setPlace('');
55+
setImage(null);
56+
setPreviewUrl(null);
57+
};
58+
59+
return (
60+
<form className="mt-4 space-y-4" onSubmit={handleSubmit}>
61+
<div className="border rounded-lg p-4 text-center bg-gray-100">
62+
{previewUrl && (
63+
<img
64+
src={previewUrl}
65+
alt="Preview"
66+
className="mx-auto w-full max-w-xs h-40 object-cover rounded mb-2"
67+
/>
68+
)}
69+
<label className="cursor-pointer underline text-gray-700">
70+
Upload Image
71+
<input
72+
type="file"
73+
accept="image/*"
74+
onChange={handleImageUpload}
75+
className="hidden"
76+
/>
77+
</label>
78+
</div>
79+
80+
<label htmlFor="title">Title</label>
81+
<input
82+
id="title"
83+
type="text"
84+
placeholder="Title Name"
85+
value={title}
86+
onChange={(e) => setTitle(e.target.value)}
87+
className="w-full border p-2 rounded"
88+
required
89+
/>
90+
91+
<label htmlFor="description">Description</label>
92+
<textarea
93+
id="description"
94+
placeholder="Description"
95+
value={description}
96+
onChange={(e) => setDescription(e.target.value)}
97+
className="w-full border p-2 rounded"
98+
required
99+
/>
100+
101+
<label htmlFor="place">Place</label>
102+
<input
103+
id="place"
104+
type="text"
105+
placeholder="Place"
106+
value={place}
107+
onChange={(e) => setPlace(e.target.value)}
108+
className="w-full border p-2 rounded"
109+
required
110+
/>
111+
112+
<button type="submit" className="w-full bg-black text-white py-2 rounded">
113+
Confirm
114+
</button>
115+
</form>
116+
);
117+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
'use client';
2+
import { useState } from 'react';
3+
4+
type Complaint = {
5+
id: number;
6+
title: string;
7+
description: string;
8+
date: string;
9+
solved: boolean;
10+
imageUrl?: string;
11+
comments?: { id: number; text: string; timestamp: string }[];
12+
};
13+
14+
type Props = {
15+
complaint: Complaint;
16+
};
17+
18+
export default function ComplaintCard({ complaint }: Props) {
19+
const [showComments, setShowComments] = useState(false);
20+
21+
return (
22+
<div className="border rounded-xl shadow-md p-4 mb-4 bg-white">
23+
{complaint.imageUrl && (
24+
<div className="w-full h-40 mb-3 rounded-md overflow-hidden bg-gray-100">
25+
<img
26+
src={complaint.imageUrl}
27+
alt="Complaint"
28+
className="w-full h-full object-cover"
29+
/>
30+
</div>
31+
)}
32+
<h2 className="text-lg font-semibold">{complaint.title}</h2>
33+
<p className="text-sm text-gray-600">
34+
Complaint Time & Date: {complaint.date}
35+
</p>
36+
<p className="mt-2 text-gray-800">{complaint.description}</p>
37+
38+
<button
39+
onClick={() => setShowComments(!showComments)}
40+
className="mt-3 text-blue-600 underline text-sm"
41+
>
42+
{showComments ? 'Hide Comments' : 'Show Comments'}
43+
</button>
44+
45+
{showComments && (
46+
<div className="mt-4">
47+
<div className="max-h-40 overflow-y-auto space-y-2 text-sm">
48+
{complaint.comments && complaint.comments.length > 0 ? (
49+
complaint.comments.map((comment) => (
50+
<div key={comment.id} className="border-b pb-1 text-gray-700">
51+
{comment.text} (at {new Date(comment.timestamp).toLocaleString()})
52+
</div>
53+
))
54+
) : (
55+
<p className="text-gray-500">No comments yet.</p>
56+
)}
57+
</div>
58+
</div>
59+
)}
60+
</div>
61+
);
62+
}

src/components/complaint/Tabs.tsx

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
type TabsProps = {
2+
selected: 'solved' | 'unsolved';
3+
onChange: (value: 'solved' | 'unsolved') => void;
4+
};
5+
6+
export default function Tabs({ selected, onChange }: TabsProps) {
7+
return (
8+
<div className="flex justify-center my-4">
9+
<button
10+
className={`px-4 py-1 rounded-l-full ${
11+
selected === 'solved'
12+
? 'bg-blue-400 text-white'
13+
: 'border border-blue-400'
14+
}`}
15+
onClick={() => onChange('solved')}
16+
>
17+
Solved
18+
</button>
19+
<button
20+
className={`px-4 py-1 rounded-r-full ${
21+
selected === 'unsolved'
22+
? 'bg-blue-400 text-white'
23+
: 'border border-blue-400'
24+
}`}
25+
onClick={() => onChange('unsolved')}
26+
>
27+
Unsolved
28+
</button>
29+
</div>
30+
);
31+
}

src/components/home/QuickActions.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ export const QuickActions = () => {
5050
},
5151
{
5252
text: "Anonymous Complaint",
53-
onClick: () => alert("Anonymous Complaint feature coming soon!"),
53+
onClick: () => (window.location.href = "/complaint"),
5454
},
5555
{
5656
text: "Private Hostel",

0 commit comments

Comments
 (0)