Skip to content

Commit 94c7bf1

Browse files
Notice_Board (#429) (#435)
2 parents 089013d + a049726 commit 94c7bf1

11 files changed

Lines changed: 739 additions & 22 deletions

File tree

.env.development

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ NEXT_PUBLIC_FRONTEND_URL=https://placement.iiti.ac.in
33
NEXTAUTH_URL=https://placement.iiti.ac.in/portal/api/auth
44
NEXT_PUBLIC_RECAPTCHA_SITE_KEY=6LedhmcrAAAAAGugyAoh4uyYPKGpOSFQf6trz-y4
55
NEXT_PUBLIC_BASE_PATH="/portal"
6-
NEXT_PUBLIC_ASSET_PREFIX="/portal"
6+
NEXT_PUBLIC_ASSET_PREFIX="/portal"
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
"use client";
2+
3+
import React, { useState, ChangeEvent, FormEvent } from "react";
4+
import { useRouter, useParams } from "next/navigation";
5+
import Image from "next/image";
6+
// import iiti from "../Images/iiti.png";
7+
8+
interface LoginInfo {
9+
clubname: string;
10+
heading: string;
11+
info: string;
12+
announcelogo: any;
13+
}
14+
15+
export default function Announce(): JSX.Element {
16+
const router = useRouter();
17+
const params = useParams();
18+
const [group, setGroup] = useState<string>("all");
19+
const [manualEmails, setManualEmails] = useState<string>("");
20+
21+
const club_name = "CAMC";
22+
23+
const [errors, setErrors] = useState<string[]>([]);
24+
const [loading, setloading] = useState<boolean>(false);
25+
26+
const [announcelogo, setannouncelogo] = useState<string>("");
27+
28+
const [logininfo, setlogininfo] = useState<LoginInfo>({
29+
clubname: "CAMC",
30+
heading: "",
31+
info: "",
32+
announcelogo:"",
33+
});
34+
35+
const handlelogochange = (e: ChangeEvent<HTMLInputElement>) => {
36+
const file = e.target.files?.[0];
37+
if (file) {
38+
const reader = new FileReader();
39+
reader.onloadend = () => {
40+
setannouncelogo(reader.result as string);
41+
setlogininfo((prev) => ({
42+
...prev,
43+
announcelogo: reader.result,
44+
}));
45+
};
46+
reader.readAsDataURL(file);
47+
}
48+
};
49+
50+
const handleChange = (
51+
e: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
52+
) => {
53+
const { name, value } = e.target;
54+
setlogininfo((prev) => ({ ...prev, [name]: value }));
55+
};
56+
57+
const handleVerification = async (email: string): Promise<boolean> => {
58+
try {
59+
const res = await fetch("http://localhost:5001/api/v1/verifyadmin", {
60+
method: "POST",
61+
headers: { "Content-Type": "application/json" },
62+
body: JSON.stringify({ email }),
63+
});
64+
const data = await res.json();
65+
return data.authorized;
66+
} catch (err) {
67+
console.error("Verification error:", err);
68+
alert("Something went wrong. Try again later.");
69+
return false;
70+
}
71+
};
72+
73+
const handleSubmit = async (e: FormEvent<HTMLFormElement>) => {
74+
e.preventDefault();
75+
setloading(true);
76+
77+
const email = prompt("Enter your email to verify admin access:");
78+
if (!email) {
79+
alert("Email is required.");
80+
return;
81+
}
82+
83+
const authorized = await handleVerification(email);
84+
85+
if (!authorized) {
86+
alert("You're not authorized to make announcements.");
87+
router.push("/admin/noticeboardview");
88+
return;
89+
}
90+
91+
try {
92+
const emailsArray = manualEmails
93+
? manualEmails.split(",").map(e => e.trim())
94+
: [];
95+
96+
const res = await fetch("http://localhost:5001/api/v1/announce", {
97+
method: "POST",
98+
headers: { "Content-Type": "application/json" },
99+
body: JSON.stringify({
100+
...logininfo,
101+
group,
102+
emails: emailsArray
103+
}),
104+
});
105+
106+
const result = await res.json();
107+
108+
if (res.ok) {
109+
alert(result.message || "Announcement successful");
110+
// setannouncelogo(iiti.src);
111+
setlogininfo({
112+
clubname: "CAMC",
113+
heading: "",
114+
info: "",
115+
announcelogo: { announcelogo },
116+
});
117+
router.push("/admin/noticeboardview");
118+
} else {
119+
if (result.errors) {
120+
setErrors(result.errors);
121+
} else {
122+
setErrors([result.message || "Signup failed"]);
123+
}
124+
}
125+
} catch (err: any) {
126+
console.error("Submit error:", err);
127+
alert("Something went wrong: " + (err.message || err));
128+
} finally {
129+
setloading(false);
130+
}
131+
};
132+
133+
return (
134+
<div className="w-full min-h-screen flex items-center justify-center bg-[#f8fafc] px-4">
135+
<div className="relative w-full md:w-[500px] bg-white border border-gray-200 rounded-xl shadow-md p-8">
136+
<button
137+
className="absolute top-4 right-4 w-[32px] h-[32px] rounded-full bg-red-500 hover:bg-red-600 text-white font-bold flex items-center justify-center transition"
138+
onClick={() => router.back()}
139+
>
140+
141+
</button>
142+
143+
<form
144+
onSubmit={handleSubmit}
145+
className="flex flex-col items-center w-full"
146+
>
147+
<h2 className="text-2xl font-bold text-[#052659] mb-6">
148+
Create Announcement
149+
</h2>
150+
151+
{errors.length > 0 && (
152+
<div className="w-full mb-4 text-center bg-red-500 text-white p-3 rounded-md">
153+
{errors.map((msg, idx) => (
154+
<p key={idx}>{msg}</p>
155+
))}
156+
</div>
157+
)}
158+
159+
<div className="w-full mb-4 px-4 py-3 border border-gray-300 rounded-lg bg-gray-50 font-semibold text-gray-800">
160+
CAMC
161+
</div>
162+
163+
<input
164+
type="text"
165+
placeholder="Announcement Heading"
166+
name="heading"
167+
value={logininfo.heading}
168+
onChange={handleChange}
169+
className="w-full mb-4 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-[#0A3A8A] outline-none"
170+
/>
171+
172+
<textarea
173+
placeholder="Announcement Info"
174+
name="info"
175+
value={logininfo.info}
176+
onChange={handleChange}
177+
className="w-full mb-6 px-4 py-3 border border-gray-300 rounded-lg h-[120px] resize-none focus:ring-2 focus:ring-[#0A3A8A] outline-none"
178+
/>
179+
180+
<div className="flex items-center justify-between w-full mb-6">
181+
<span className="font-semibold text-gray-700">
182+
Announcement Logo
183+
</span>
184+
185+
<label className="w-20 h-20 rounded-lg overflow-hidden border border-gray-300 cursor-pointer shadow-sm hover:shadow-md transition">
186+
<img
187+
src={announcelogo}
188+
className="w-full h-full object-cover"
189+
alt="logo"
190+
/>
191+
192+
<input
193+
type="file"
194+
accept="image/*"
195+
className="hidden"
196+
onChange={handlelogochange}
197+
/>
198+
</label>
199+
</div>
200+
<select
201+
aria-label="Select group"
202+
value={group}
203+
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
204+
setGroup(e.target.value)
205+
}
206+
className="w-full mb-4 px-4 py-3 border border-gray-300 rounded-lg"
207+
>
208+
{/* Here need to add the names to the email */}
209+
<option value="all">All Students</option>
210+
<option value="cse">CSE</option>
211+
<option value="ece">ECE</option>
212+
</select>
213+
214+
<input
215+
type="text"
216+
placeholder="Extra emails (comma separated)"
217+
value={manualEmails}
218+
onChange={(e) => setManualEmails(e.target.value)}
219+
className="w-full mb-4 px-4 py-3 border border-gray-300 rounded-lg"
220+
/>
221+
<button
222+
type="submit"
223+
className="w-full py-3 rounded-lg bg-[#0A3A8A] hover:bg-[#052659] text-white font-bold text-lg transition shadow-sm"
224+
>
225+
Publish Announcement
226+
</button>
227+
</form>
228+
</div>
229+
</div>
230+
);
231+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
"use client";
2+
3+
import { useEffect, useState } from "react";
4+
5+
interface NotificationItem {
6+
announcelogo: string;
7+
heading: string;
8+
info: string;
9+
}
10+
11+
export default function NoticeBoardView(): JSX.Element {
12+
const [notifi, setNotifi] = useState<NotificationItem[]>([]);
13+
const [selectedImage, setSelectedImage] = useState<string | null>(null);
14+
15+
useEffect(() => {
16+
const fetchAnnouncements = async () => {
17+
try {
18+
const res = await fetch(
19+
"http://localhost:5001/api/v1/notification"
20+
);
21+
const data = await res.json();
22+
23+
setNotifi(data.reverse());
24+
} catch (err) {
25+
console.error("Failed to load notifications:", err);
26+
}
27+
};
28+
29+
fetchAnnouncements();
30+
}, []);
31+
32+
return (
33+
<div className="w-full min-h-screen bg-[#f8fafc] flex flex-col items-center py-12 px-4">
34+
35+
<h1 className="text-3xl md:text-4xl font-bold text-[#052659] mb-10 tracking-wide">
36+
Announcements
37+
</h1>
38+
39+
{notifi.length === 0 ? (
40+
<div className="flex flex-col items-center space-y-4 text-gray-500 mt-16">
41+
<img
42+
src="https://cdn-icons-png.flaticon.com/512/4076/4076549.png"
43+
alt="No notifications"
44+
className="w-28 h-28 opacity-80"
45+
/>
46+
<p className="text-lg font-medium">
47+
No announcements available
48+
</p>
49+
</div>
50+
) : (
51+
<div className="w-full md:w-[900px] flex flex-col gap-6">
52+
53+
{notifi.map((e, index) => (
54+
<div
55+
key={index}
56+
className="group bg-white border border-gray-200 rounded-xl p-6 flex items-start justify-between gap-6 shadow-sm hover:shadow-lg transition duration-300"
57+
>
58+
59+
<div className="flex items-start gap-4 flex-1">
60+
<div className="w-[4px] bg-[#0A3A8A] rounded-full"></div>
61+
62+
<div>
63+
<h2 className="text-lg md:text-xl font-semibold text-[#052659] group-hover:text-[#0A3A8A] transition">
64+
{e.heading}
65+
</h2>
66+
67+
<div className="w-12 h-[2px] bg-gray-200 my-2"></div>
68+
69+
<p className="text-gray-700 text-sm md:text-base leading-relaxed">
70+
{e.info}
71+
</p>
72+
</div>
73+
</div>
74+
75+
<div className="flex-shrink-0">
76+
<img
77+
src={e.announcelogo}
78+
alt="announcement"
79+
onClick={() => setSelectedImage(e.announcelogo)}
80+
className="h-[60px] w-[60px] object-cover rounded-lg border border-gray-300 shadow-sm cursor-pointer hover:scale-110 transition duration-300"
81+
/>
82+
</div>
83+
84+
</div>
85+
))}
86+
87+
</div>
88+
)}
89+
90+
{selectedImage && (
91+
<div
92+
className="fixed inset-0 bg-black bg-opacity-70 flex items-center justify-center z-50"
93+
onClick={() => setSelectedImage(null)}
94+
>
95+
<div className="relative">
96+
97+
<button
98+
onClick={() => setSelectedImage(null)}
99+
className="absolute -top-8 right-0 text-white text-3xl font-bold"
100+
>
101+
102+
</button>
103+
104+
<img
105+
src={selectedImage}
106+
alt="preview"
107+
className="max-h-[80vh] max-w-[90vw] rounded-xl shadow-lg"
108+
/>
109+
</div>
110+
</div>
111+
)}
112+
113+
</div>
114+
);
115+
}

src/app/globals.css

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,3 @@ body {
2020
color: rgb(var(--foreground-rgb));
2121
background: rgb(232, 232, 232);
2222
}
23-
24-
/* Removes square blue outline */
25-
.ant-select-single .ant-select-selection-search-input:focus {
26-
outline: none !important;
27-
box-shadow: none !important;
28-
}

src/components/Admin/ExternalOpportunities.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ export const AddExternalOpportunity = ({
6666
};
6767

6868
return (
69+
6970
<Dialog open={open} onOpenChange={setOpen}>
7071
<DialogContent className="sm:max-w-[425px]">
7172
<DialogHeader>

0 commit comments

Comments
 (0)