From 0b2096460fca1ef8c8ba9909e9f4f395a7a911cb Mon Sep 17 00:00:00 2001 From: Yashika Choudhary <161009245+yashika1221@users.noreply.github.com> Date: Sat, 3 Jan 2026 22:34:20 +0530 Subject: [PATCH 01/73] Gallery Component and update to IKS cell page Co-authored-by: Aryawart-kathpal --- app/[locale]/institute/cells/iks/page.tsx | 168 +++++++++++++--- components/ui/gallery.tsx | 227 ++++++++-------------- i18n/en.ts | 66 ++++--- i18n/hi.ts | 77 ++++---- i18n/translations.ts | 15 +- 5 files changed, 312 insertions(+), 241 deletions(-) diff --git a/app/[locale]/institute/cells/iks/page.tsx b/app/[locale]/institute/cells/iks/page.tsx index d09a5d6fc..165101179 100644 --- a/app/[locale]/institute/cells/iks/page.tsx +++ b/app/[locale]/institute/cells/iks/page.tsx @@ -9,39 +9,126 @@ export default async function IKS({ }) { const text = await getTranslations(locale); - const description = - 'IKS Cell is an innovative cell which is established in 2022 in the Institute. It is established to promote interdisciplinary research on all aspects of IKS, preserve and disseminate IKS for further research and societal applications. It will actively engage in spreading the rich heritage of our country and traditional knowledge in the field of Psychology, Basic Sciences, Engineering & Technology, Arts and literature, Agriculture, Architecture etc.'; const activities = [ { id: 1, description: - 'Expert talk on “Quality Life Management and Professional Excellence” by Prof Navneet Arora, IIT Roorkee on 15.01.2024 at 9:30 AM (1 hour duration) in the Seminar Hall of the Computer Engineering Department.', + 'IKS Cell, National Institute of Technology, Kurukshetra (Haryana) India organized a 5-Day Workshop on “Stress Management & Professional Excellence” from 24-28 November 2023.', }, { id: 2, description: - 'IKS Cell Celebrated Pran Pratishtha Ceremony of Lord Rama at Ayodhya on January 22, 2024. Bhagirathi Bhawan at 11 AM.', + 'IKS Cell, National Institute of Technology, Kurukshetra (Haryana) India organized a One Day National Conference on Psychology, Science & Technology (PST-IKS 2023) as Conference Secretary on 23 December 2023.', }, { id: 3, description: - 'IKS Cell organized a Five Days Workshop on “Stress Management and Professional Excellence” during November 24–28, 2023.', + 'Expert talk on “Quality Life Management and Professional Excellence” by Prof. Navneet Arora, IIT Roorkee on 15.01.2024 at 9:30 AM (1 hour duration) in the Seminar Hall of the Computer Engineering Department, NIT Kurukshetra.', + }, + { + id: 4, + description: + 'IKS Cell celebrated the Pran Pratishtha Ceremony of Lord Rama at Ayodhya on 22.01.2024 at Bhagirathi Bhawan, 11 AM.', + }, + { + id: 5, + description: + 'IKS Cell performed Hawan on 11.10.2024 at Kalpna Chawla Bhawan, Girls Hostel, 11 AM.', + }, + { + id: 6, + description: + 'Performed Hawan at Institute Health Centre on 15.10.2023, NIT Kurukshetra.', + }, + { + id: 7, + description: + 'Lecture Series for Rural School Students by Shri Acharya Shivanand Ji from Varanasi, held on 04.12.2024 and 06.12.2024.', + }, + { + id: 8, + description: + 'Expert talk on “Mental Harmony & Meditation” by Mr. Rudransh Aggarwal, IIT Roorkee on 23.05.2025 at 10:00 AM (1 hour duration) in the Seminar Hall, Computer Engineering Department, NIT Kurukshetra.', }, ]; - const members = [ + const team = [ { - name: 'Prof RK Aggarwal', - designation: 'Prof-In-Charge, IKS Cell', + name: 'Prof. R.K. Aggarwal', + designation: 'Professor-In-Charge, IKS Cell, NIT Kurukshetra', }, { name: 'Dr. Shabnam', - designation: 'Faculty-In-Charge, IKS Cell', + designation: 'Faculty-In-Charge, IKS Cell, NIT Kurukshetra', + }, + { + name: 'Dr. Sachin Maheshwari', + designation: 'Vice Chancellor, GJU Moradabad, UP', + }, + { + name: 'Dr. Rajesh Raj', + designation: 'Director, Ritanveshi Yogayan Foundation', + }, + { + name: 'Dr. Jagan Nath', + designation: + 'Sr. Tech. Officer, Ramchandra Mission (Heartfulness), NIT Kurukshetra', }, { name: 'Dr. Kuldeep Kumar', - designation: 'Faculty-In-Charge, IKS Cell', + designation: 'Assistant Professor, NIT Kurukshetra', + }, + { name: 'Dr. Manasa Reddy', designation: 'Psychologist, NIT Kurukshetra' }, + { + name: 'Dr. Navneet Arora', + designation: 'Professor, IIT Roorkee, Dev Samaj', + }, + { name: 'Dr. Navneet', designation: 'Dean, Gurukul Kangri, Haridwar' }, + { + name: 'Dr. Sanjay Sharma', + designation: 'Professor, Gautam Buddha University, UP', + }, + { name: 'Dr. Sandeep Arya', designation: 'Dean Faculty, GJU Hisar' }, + { name: 'Shri Nakul Vashishtha', designation: 'Entrepreneur' }, + { name: 'Dr. Amita Mittal', designation: 'Assistant Professor, KUK' }, + { name: 'Mr. Rudransh Aggarwal', designation: 'B.Tech, IIT Roorkee' }, + { name: 'Dr. Sonam Nagar', designation: 'Brahmkumaris, Gurugram' }, + { + name: 'Shri Mithlesh Kumar Singh', + designation: 'Deputy Director, Electrical Safety UP', + }, + { name: 'Shri Ram Kumar Sharma', designation: 'Vidya Vistaar Yojana' }, + { + name: 'Dr. Diksha Arya', + designation: 'Assistant Professor, University of Tokyo, Japan', + }, + { + name: 'Dr. Kapil Bhatt', + designation: 'Assistant Professor, HP University, Shimla', + }, + ]; + const coordinators = [ + { name: 'Mr Chandan', education: 'M.Tech' }, + { name: 'Mr Sanjay', education: 'B.Tech' }, + { name: 'Ms Priya Kumari', education: 'M.Tech' }, + { name: 'Mr. Ashfaq KP Jafar', education: 'PhD.' }, + ]; + + const book = [ + { + author: 'Aggarwal RK', + title: '(2025). Divine Science of KundaliniKriya Yoga.IndicaInfomedia.', + }, + { author: 'Aggarwal RK', title: 'Yogi Charitamrit' }, + { + author: 'Aggarwal RK', + title: 'A Manual For Healthy Life And Healthy India (Vol. I & II)', }, ]; + + const galleryImages = Array.from({ length: 23 }, (_, i) => ({ + src: `institute/cells/iks/${i + 1}.jpg`, + })); + return ( <> {/* heading */} @@ -68,39 +155,76 @@ export default async function IKS({

{text.Institute.cells.iks.title}

- {text.Institute.cells.iks.description} + {text.Institute.cells.iks.description[0]} +

+
+
+

+ {text.Institute.cells.iks.description[1]}

- {/* IKS Team */} + {/* IKS Team and Student Coordinators */}

{text.Institute.cells.iks.iksTeam}

- + + +

+ {text.Institute.cells.iks.coordinators} +

+
    + {coordinators.map((coordinators) => ( +
  1. + {coordinators.name}, {coordinators.education} +
  2. + ))} +
{/* Activities Performed in Year 2023-2024 */}

- Activities Performed in Year 2023-2024 + {text.Institute.cells.iks.activitiesPerformed}

-
    +
      {activities.map((act) => (
    • {act.description}
    • ))} -
+
+ + {/* Book Release */} +
+

+ {text.Institute.cells.iks.book} +

+ +
+ {/* Gallery */}
-

Gallery

- +

+ {text.Institute.cells.iks.imageGallery} +

+
diff --git a/components/ui/gallery.tsx b/components/ui/gallery.tsx index fffe85bcb..dd043639e 100644 --- a/components/ui/gallery.tsx +++ b/components/ui/gallery.tsx @@ -5,190 +5,119 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; export interface Img { src: string; - alt: string; } interface GalleryProps { base: string; + images: Img[]; + viewMoreText: string; } -// Row patterns -const rowPatterns: ('h' | 'v')[][] = [ - ['h', 'v', 'h', 'v'], - ['h', 'h', 'h'], - ['v', 'h', 'v', 'h'], - ['h', 'h', 'h'], -]; - -export default function Gallery({ base }: GalleryProps) { - // static images with base from server + random vertical images for better testing - const images: Img[] = useMemo( - () => [ - { src: `${base}/assets/mahabharat.jpeg`, alt: 'Mahabharat Illustration' }, - { src: `${base}/academics/0.jpg`, alt: 'Academic Building View 1' }, - { src: `${base}/academics/1.jpg`, alt: 'Academic Building View 2' }, - { src: `${base}/academics/2.jpg`, alt: 'Academic Building View 3' }, - { src: `${base}/events/image3.jpg`, alt: 'Campus Event Celebration' }, - { src: `${base}/hostels/gh1.webp`, alt: 'Girls Hostel Exterior View 1' }, - { src: `${base}/hostels/gh2.webp`, alt: 'Girls Hostel Exterior View 2' }, - { src: `${base}/hostels/gh3.webp`, alt: 'Girls Hostel Exterior View 3' }, - { src: `${base}/hostels/h1.webp`, alt: 'Boys Hostel Exterior View 1' }, - { src: `${base}/hostels/h2.webp`, alt: 'Boys Hostel Exterior View 2' }, - { src: `${base}/hostels/h3.webp`, alt: 'Boys Hostel Exterior View 3' }, - { src: `${base}/hostels/h4.webp`, alt: 'Boys Hostel Exterior View 4' }, - { src: `${base}/hostels/h5.webp`, alt: 'Boys Hostel Exterior View 5' }, - { src: `${base}/hostels/h6.webp`, alt: 'Boys Hostel Exterior View 6' }, - { src: `${base}/hostels/h7.webp`, alt: 'Boys Hostel Exterior View 7' }, - { src: `${base}/hostels/h8.webp`, alt: 'Boys Hostel Exterior View 8' }, - { src: `${base}/institute/image01.jpg`, alt: 'Main Institute Building' }, - { src: `${base}/assets/mahabharat.jpeg`, alt: 'Mahabharat Illustration' }, - { src: `${base}/academics/0.jpg`, alt: 'Academic Building View 1' }, - { src: `${base}/academics/1.jpg`, alt: 'Academic Building View 2' }, - { src: `${base}/academics/2.jpg`, alt: 'Academic Building View 3' }, - { src: `${base}/events/image3.jpg`, alt: 'Campus Event Celebration' }, - { src: `${base}/hostels/gh1.webp`, alt: 'Girls Hostel Exterior View 1' }, - { src: `${base}/hostels/gh2.webp`, alt: 'Girls Hostel Exterior View 2' }, - { src: `${base}/hostels/gh3.webp`, alt: 'Girls Hostel Exterior View 3' }, - { src: `${base}/hostels/h1.webp`, alt: 'Boys Hostel Exterior View 1' }, - { src: `${base}/hostels/h2.webp`, alt: 'Boys Hostel Exterior View 2' }, - { src: `${base}/hostels/h3.webp`, alt: 'Boys Hostel Exterior View 3' }, - { src: `${base}/hostels/h4.webp`, alt: 'Boys Hostel Exterior View 4' }, - { src: `${base}/hostels/h5.webp`, alt: 'Boys Hostel Exterior View 5' }, - { src: `${base}/hostels/h6.webp`, alt: 'Boys Hostel Exterior View 6' }, - { src: `${base}/hostels/h7.webp`, alt: 'Boys Hostel Exterior View 7' }, - { src: `${base}/hostels/h8.webp`, alt: 'Boys Hostel Exterior View 8' }, - { src: `${base}/institute/image01.jpg`, alt: 'Main Institute Building' }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - { - src: `https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ8qwnKumpD9IrCm6nx2f0ndrQ9p-vNxee2VQ&s`, - alt: 'Main Institute Building', - }, - ], - [base] - ); +type ClassifiedImg = Img & { type: 'h' | 'v' }; - const [horizontal, setHorizontal] = useState<(Img & { type: 'h' | 'v' })[]>( - [] - ); - const [vertical, setVertical] = useState<(Img & { type: 'h' | 'v' })[]>([]); +export default function Gallery({ base, images, viewMoreText }: GalleryProps) { + const [horizontal, setHorizontal] = useState([]); + const [vertical, setVertical] = useState([]); - // Classify images as horizontal or vertical + // Classify Images useEffect(() => { + if (!base) return; + void Promise.all( images.map( (img) => - new Promise((resolve) => { + new Promise((resolve) => { + const fullSrc = `${base}/${img.src}`; const temp = new window.Image(); - temp.src = img.src; + temp.src = fullSrc; + temp.onload = () => resolve({ - ...img, + src: fullSrc, type: temp.naturalWidth > temp.naturalHeight ? 'h' : 'v', }); - temp.onerror = () => resolve({ ...img, type: 'v' }); + + temp.onerror = () => resolve({ src: fullSrc, type: 'v' }); }) ) ).then((classified) => { setHorizontal(classified.filter((i) => i.type === 'h')); setVertical(classified.filter((i) => i.type === 'v')); }); - }, [images]); + }, [base, images]); - // Merge images according to row patterns + // Build rows using patterns const rows = useMemo(() => { - const mergedRows: (Img & { type: 'h' | 'v' })[][] = []; - let hIndex = 0, - vIndex = 0; - let patternIndex = 0; - - while (hIndex < horizontal.length || vIndex < vertical.length) { - const pattern = rowPatterns[patternIndex % rowPatterns.length]; - const row: (Img & { type: 'h' | 'v' })[] = []; + const mergedRows: ClassifiedImg[][] = []; + let hIndex = 0; + let vIndex = 0; + let n = 0; + const hArr = horizontal; + const vArr = vertical; + + while (hIndex < hArr.length || vIndex < vArr.length) { + const remainingH = hArr.length - hIndex; + const remainingV = vArr.length - vIndex; + const totalRemaining = remainingH + remainingV; + let pattern: ('h' | 'v')[] = []; + + if (totalRemaining > 0 && totalRemaining <= 3) { + for (let i = 0; i < remainingH; i++) pattern.push('h'); + for (let i = 0; i < remainingV; i++) pattern.push('v'); + } else { + const mod4 = n % 4; + if (mod4 === 0 && remainingH >= 2 && remainingV >= 2) + pattern = ['h', 'v', 'h', 'v']; + else if ((mod4 === 1 || mod4 === 3) && remainingH >= 3) + pattern = ['h', 'h', 'h']; + else if (mod4 === 2 && remainingH >= 2 && remainingV >= 2) + pattern = ['v', 'h', 'v', 'h']; + else if (remainingH >= 2 && remainingV >= 1) pattern = ['h', 'v', 'h']; + else if (remainingH >= 1 && remainingV >= 2) pattern = ['v', 'h', 'v']; + else if (remainingH > 0) + pattern = Array(Math.min(remainingH, 3)).fill('h') as ('h' | 'v')[]; + else if (remainingV > 0) + pattern = Array(Math.min(remainingV, 4)).fill('v') as ('h' | 'v')[]; + } + const row: ClassifiedImg[] = []; for (const type of pattern) { - if (type === 'h' && hIndex < horizontal.length) { - row.push(horizontal[hIndex]); + if (type === 'h' && hIndex < hArr.length) { + row.push(hArr[hIndex]); hIndex++; - } else if (type === 'v' && vIndex < vertical.length) { - row.push(vertical[vIndex]); + } else if (type === 'v' && vIndex < vArr.length) { + row.push(vArr[vIndex]); vIndex++; } } if (row.length > 0) mergedRows.push(row); - patternIndex++; + n++; } return mergedRows; }, [horizontal, vertical]); - // Show 3 rows at a time const [visibleRowCount, setVisibleRowCount] = useState(3); - - const handleViewMore = useCallback(() => { - setVisibleRowCount((prev) => prev + 3); - }, []); - - // Get visible rows and flatten them + const handleViewMore = useCallback( + () => setVisibleRowCount((prev) => prev + 3), + [] + ); const visibleRows = rows.slice(0, visibleRowCount); const visibleImages = visibleRows.flat(); const hasMoreRows = visibleRowCount < rows.length; - - // Calculate which image should show the View More button const viewMorePosition = hasMoreRows ? visibleImages.length - 1 : -1; - return ( -
+ return ( +
{visibleRows.map((row, rowIdx) => ( -
+
{row.map((img, idx) => { + console.log('RENDERING IMAGE SRC →', img.src); + const globalIndex = row.slice(0, idx + 1).length + rows.slice(0, rowIdx).flat().length - @@ -200,19 +129,19 @@ export default function Gallery({ base }: GalleryProps) { key={`${img.src}-${idx}`} className={`relative overflow-hidden rounded ${ isViewMorePosition ? '' : 'border-2 border-primary-300' + } ${ + img.type === 'h' + ? 'aspect-[4/3] w-full flex-[2] sm:w-[48%] lg:max-w-[400px]' + : 'aspect-[2/3] w-full flex-[1] sm:w-[30%] lg:max-w-[192px]' }`} - style={img.type === 'h' ? { width: 400, height: 300 } : { width: 192, height: 300 }} > {img.alt} - {isViewMorePosition && ( )}
@@ -232,4 +161,4 @@ export default function Gallery({ base }: GalleryProps) { ))}
); -} \ No newline at end of file +} diff --git a/i18n/en.ts b/i18n/en.ts index 2df3350d7..0c9721d18 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -185,6 +185,7 @@ const text: Translations = { examDateSheet: 'Exam Date Sheet', timeTable: 'Time Table', }, + viewMore: 'View More', }, Academics: { notifications: 'Notifications', @@ -616,78 +617,83 @@ const text: Translations = { }, iks: { title: 'Indian Knowledge Systems', - description: - 'IKS Cell is an innovative cell established in 2022 in the Institute. It aims to promote interdisciplinary research on all aspects of Indian Knowledge Systems, preserve and disseminate IKS for further research and societal applications. The cell will actively engage in spreading the rich heritage of our country and traditional knowledge in fields such as Psychology, Basic Sciences, Engineering & Technology, Arts and Literature, Agriculture, Architecture, and more.', - iksTeam: 'IKS Team', + description: [ + `The Indian Knowledge Systems (IKS) Cell is an innovative initiative established in 2022 at the Institute, under the aegis of the Ministry of Education, Government of India. It was launched with the vision of promoting and integrating India’s rich intellectual traditions into modern academic and research frameworks. Rooted in the diverse cultural and philosophical heritage of our nation, the IKS Cell is committed to fostering interdisciplinary research that draws upon indigenous knowledge systems and practices.`, + `The Cell’s primary objective is to explore, preserve, and disseminate traditional Indian knowledge across a wide array of disciplines, including Basic Sciences, Engineering and Technology, Psychology, Arts and Literature, Agriculture, and Architecture. It aims to bridge the gap between ancient wisdom and contemporary scientific inquiry, ensuring that India’s time-honoured approaches are not only preserved but also adapted to address current societal challenges.`, + ], + iksTeam: 'Team IKS Cell, NIT Kurukshetra', + coordinators: 'Student Coordinators, IKS Cell, NIT Kurukshetra', + activitiesPerformed: 'Activities Performed in Year 2023-2024', + book: 'Book Release IKS Cell, NIT Kurukshetra', + imageGallery: 'Image Gallery', }, + ipr: { title: 'Intellectual Property Rights', }, scst: { title: 'SC & ST Cell', - description: - [ - 'NIT Kurukshetra is committed to maintaining a work environment wherein students, faculty, and staff members from different community can work in a coherent environment. It is the institute\'s endeavor to ensure that no discrimination takes place at workplace.', + description: [ + "NIT Kurukshetra is committed to maintaining a work environment wherein students, faculty, and staff members from different community can work in a coherent environment. It is the institute's endeavor to ensure that no discrimination takes place at workplace.", 'The Institute has appointed a Liaison Officer for SC & ST cell who can be contacted in the event of any incident of caste-based discrimination.', - 'SC & ST cell has been constituted in NIT-Kurukshetra (An Institution of National Importance) w.e.f. 24th August, 2017 as per the instructions of the Government of India, Ministry of Personal, Public Grievances and Pension (Department of Personal and Training) vide office memorandum No. 43011/153/2010-Estt.(Res) dated 4th January 2013.' - ], - cellFunctionsHeading:'CELL FUNCTIONS', - cellFunctions: - [ + 'SC & ST cell has been constituted in NIT-Kurukshetra (An Institution of National Importance) w.e.f. 24th August, 2017 as per the instructions of the Government of India, Ministry of Personal, Public Grievances and Pension (Department of Personal and Training) vide office memorandum No. 43011/153/2010-Estt.(Res) dated 4th January 2013.', + ], + cellFunctionsHeading: 'CELL FUNCTIONS', + cellFunctions: [ 'Grievances redress the grievances of SC/ST students and employees and render them necessary help in solving their academic as well as administrative problems.', 'Monitors and evaluates the reservation policies and other programs intended for SC/STs by the Government of India for their effective implementation at National Institute of Technology Kurukshetra.', 'Suggests the follow-up measures to the administration of the institute to achieve the objectives and targets laid down by MHRD for the empowerment of SC/STs.', 'To register the complaints of SC/ST students/employees of the Institute for their representation to the administration for taking further necessary action.', - 'Ensuring due compliance by the subordinate appointing authorities with the orders and instructions pertaining to the reservation of vacancies in favour of Scheduled Castes, Scheduled Tribes and Other Backward Classes and other benefits admissible to them.' + 'Ensuring due compliance by the subordinate appointing authorities with the orders and instructions pertaining to the reservation of vacancies in favour of Scheduled Castes, Scheduled Tribes and Other Backward Classes and other benefits admissible to them.', ], - complaint: 'In case you want to register a formal complaint, please fill out the form in the complaint book, which is available in SC & ST Cell, Administrative Building, NIT Kurukshetra. The committee will look into the discrimination complaints received from SC & ST Students, faculty, and staff members and resolve such complaints.', + complaint: + 'In case you want to register a formal complaint, please fill out the form in the complaint book, which is available in SC & ST Cell, Administrative Building, NIT Kurukshetra. The committee will look into the discrimination complaints received from SC & ST Students, faculty, and staff members and resolve such complaints.', liaisonOfficerHeading: 'LIAISON OFFICER', - liaisonOfficer : { + liaisonOfficer: { image: 'fallback/user-image.jpg', name: 'Arun Goel', title: 'Professor (Head of the Department)', email: 'drarun_goel@yahoo.co.in', - phone: '01744-233349, 01744-233300' + phone: '01744-233349, 01744-233300', }, importantLinksHeading: 'IMPORTANT LINKS', - importantLinks: - [ + importantLinks: [ { title: 'Ministry of Social Justice and Empowerment', - link: 'https://socialjustice.gov.in' + link: 'https://socialjustice.gov.in', }, { title: 'List of Scheduled Castes', - link: 'https://socialjustice.gov.in/common/76750' + link: 'https://socialjustice.gov.in/common/76750', }, { title: 'List of Scheduled Tribes', - link: 'https://cdnbbsr.s3waas.gov.in/s301894d6f048493d2cacde3c579c315a3/uploads/2022/03/2022030426.pdf' + link: 'https://cdnbbsr.s3waas.gov.in/s301894d6f048493d2cacde3c579c315a3/uploads/2022/03/2022030426.pdf', }, { title: 'National Commission for Scheduled Cast, GoI', - link: 'https://ncsc.nic.in' + link: 'https://ncsc.nic.in', }, { title: 'National Commission for Scheduled Tribes, GoI', - link: 'https://ncstgrams.gov.in' + link: 'https://ncstgrams.gov.in', }, { title: 'SC & ST Cell AICTE', - link: 'https://www.aicte.gov.in/bureaus/administration/scst-cell' - } + link: 'https://www.aicte.gov.in/bureaus/administration/scst-cell', + }, ], }, obcpwd: { title: 'OBC & PWD Cell', description: [ - 'NIT Kurukshetra is committed to maintaining a work environment where students, faculty, and staff members from different communities can work together harmoniously. It is the institute\'s endeavor to ensure that no discrimination takes place in the workplace. The Institute has appointed a Liaison Officer for the OBC Cell, who can be contacted in the event of any caste-based discrimination.' + "NIT Kurukshetra is committed to maintaining a work environment where students, faculty, and staff members from different communities can work together harmoniously. It is the institute's endeavor to ensure that no discrimination takes place in the workplace. The Institute has appointed a Liaison Officer for the OBC Cell, who can be contacted in the event of any caste-based discrimination.", ], cellFunctionsHeading: 'CELL FUNCTIONS', cellFunctions: [ 'To ensure proper implementation of various schemes of MHRD, GoI, and the State Government concerning scholarships, stipends, etc., for the welfare of reserved categories.', 'Grievance Redressal: for any grievance(s) regarding academic, administrative, or social issues. The cell takes necessary action and provides advice/help to resolve the matter.', - 'To take follow-up measures to achieve the objectives and targets laid down by MHRD, Government of India.' + 'To take follow-up measures to achieve the objectives and targets laid down by MHRD, Government of India.', ], complaint: 'In case you want to register a formal complaint, please fill out the form in the complaint book, available in the OBC Cell, Administrative Building, NIT Kurukshetra. The committee will review discrimination complaints received from OBC students, faculty, and staff members and resolve them accordingly.', @@ -697,15 +703,15 @@ const text: Translations = { name: 'Arun Goel', title: 'Professor & Head of Department', email: 'drarun_goel@yahoo.co.in', - phone: '01744-233349, 01744-233300' - } - } + phone: '01744-233349, 01744-233300', + }, + }, }, }, NotFound: { title: '404', description: 'Not found ', - backHome: 'Looks like you\'re lost let\'s get you back home', + backHome: "Looks like you're lost let's get you back home", }, Profile: { tabs: { diff --git a/i18n/hi.ts b/i18n/hi.ts index ce5dbb656..ec9ca63aa 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -180,6 +180,7 @@ const text: Translations = { examDateSheet: 'परीक्षा दिनांक पत्र', timeTable: 'समय-सारणी', }, + viewMore: 'और देखें', }, Academics: { notifications: 'सूचनाएँ', @@ -565,92 +566,96 @@ const text: Translations = { }, iks: { title: 'भारतीय ज्ञान प्रणाली', - description: - 'आईकेएस प्रकोष्ठ एक नवाचारी इकाई है जिसे वर्ष 2022 में संस्थान में स्थापित किया गया। इसका उद्देश्य भारतीय ज्ञान प्रणाली के सभी पहलुओं पर अंतःविषयक शोध को प्रोत्साहित करना, IKS को संरक्षित करना और आगे के शोध तथा सामाजिक अनुप्रयोगों के लिए उसका प्रसार करना है। यह प्रकोष्ठ हमारे देश की समृद्ध धरोहर और पारंपरिक ज्ञान को मनोविज्ञान, मूलभूत विज्ञान, अभियंत्रण एवं प्रौद्योगिकी, कला और साहित्य, कृषि, वास्तुकला आदि क्षेत्रों में सक्रिय रूप से फैलाने का कार्य करेगा।', - iksTeam: 'आईकेएस टीम', + description: [ + `भारतीय ज्ञान प्रणाली (IKS) प्रकोष्ठ संस्थान में वर्ष 2022 में भारत सरकार के शिक्षा मंत्रालय के तत्वावधान में स्थापित की गई एक नवाचारी पहल है। इसकी स्थापना का उद्देश्य भारत की समृद्ध बौद्धिक परंपराओं को आधुनिक शैक्षणिक एवं शोध ढाँचों में प्रोत्साहित करना और एकीकृत करना है। हमारी राष्ट्र की विविध सांस्कृतिक और दार्शनिक विरासत में निहित यह प्रकोष्ठ स्वदेशी ज्ञान प्रणालियों और प्रथाओं पर आधारित अंतःविषयक अनुसंधान को बढ़ावा देने के लिए प्रतिबद्ध है।`, + `इस प्रकोष्ठ का मुख्य उद्देश्य मूल विज्ञान, अभियांत्रिकी एवं प्रौद्योगिकी, मनोविज्ञान, कला एवं साहित्य, कृषि तथा वास्तुकला सहित विभिन्न विषयों में पारंपरिक भारतीय ज्ञान का अन्वेषण, संरक्षण एवं प्रसार करना है। यह प्राचीन ज्ञान और समकालीन वैज्ञानिक अनुसंधान के बीच की खाई को पाटने का प्रयास करता है, ताकि भारत की कालजयी ज्ञान परंपराएँ न केवल संरक्षित रहें, बल्कि वर्तमान सामाजिक चुनौतियों के समाधान हेतु उन्हें प्रासंगिक रूप में अपनाया जा सके।`, + ], + iksTeam: 'आईकेएस प्रकोष्ठ टीम, एनआईटी कुरुक्षेत्र', + coordinators: 'छात्र समन्वयक, आईकेएस प्रकोष्ठ, एनआईटी कुरुक्षेत्र', + activitiesPerformed: + 'आईकेएस प्रकोष्ठ,द्वारा आयोजित गतिविधियाँ 2023-2024', + book: 'पुस्तक विमोचन, आईकेएस प्रकोष्ठ, एनआईटी कुरुक्षेत्र', + imageGallery: 'छवि गैलरी', }, ipr: { title: 'बौद्धिक संपदा अधिकार', }, scst: { title: 'अनुसूचित जाति & अनुसूचित जनजाति प्रकोष्ठ', - description: - [ + description: [ 'एनआईटी कुरुक्षेत्र एक ऐसे कार्य वातावरण को बनाए रखने के लिए प्रतिबद्ध है जिसमें विभिन्न समुदायों के छात्र, शिक्षक और कर्मचारी सदस्य एक सुसंगत वातावरण में काम कर सकें। यह संस्थान का प्रयास है कि कार्यस्थल पर कोई भेदभाव न हो।', 'संस्थान ने अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ के लिए एक संपर्क अधिकारी नियुक्त किया है जिनसे जाति-आधारित भेदभाव की किसी भी घटना की स्थिति में संपर्क किया जा सकता है।', - 'एनआईटी-कुरुक्षेत्र (राष्ट्रीय महत्व का एक संस्थान) में अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ का गठन 24 अगस्त, 2017 से भारत सरकार, कार्मिक, लोक शिकायत और पेंशन मंत्रालय (कार्मिक और प्रशिक्षण विभाग) के निर्देशों के अनुसार कार्यालय ज्ञापन संख्या 43011/153/2010-स्था.(आर) दिनांक 4 जनवरी 2013 के अनुसार किया गया है।' - ], + 'एनआईटी-कुरुक्षेत्र (राष्ट्रीय महत्व का एक संस्थान) में अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ का गठन 24 अगस्त, 2017 से भारत सरकार, कार्मिक, लोक शिकायत और पेंशन मंत्रालय (कार्मिक और प्रशिक्षण विभाग) के निर्देशों के अनुसार कार्यालय ज्ञापन संख्या 43011/153/2010-स्था.(आर) दिनांक 4 जनवरी 2013 के अनुसार किया गया है।', + ], cellFunctionsHeading: 'प्रकोष्ठ के कार्य', - cellFunctions: - [ + cellFunctions: [ 'अनुसूचित जाति/अनुसूचित जनजाति के छात्रों और कर्मचारियों की शिकायतों का निवारण करना और उन्हें उनकी शैक्षणिक और प्रशासनिक समस्याओं को हल करने में आवश्यक सहायता प्रदान करना।', 'राष्ट्रीय प्रौद्योगिकी संस्थान कुरुक्षेत्र में उनके प्रभावी कार्यान्वयन के लिए भारत सरकार द्वारा अनुसूचित जाति/अनुसूचित जनजाति के लिए आरक्षण नीतियों और अन्य कार्यक्रमों की निगरानी और मूल्यांकन करना।', 'अनुसूचित जाति/अनुसूचित जनजाति के सशक्तिकरण के लिए मानव संसाधन विकास मंत्रालय द्वारा निर्धारित उद्देश्यों और लक्ष्यों को प्राप्त करने के लिए संस्थान के प्रशासन को अनुवर्ती उपाय सुझाना।', 'संस्थान के अनुसूचित जाति/अनुसूचित जनजाति छात्रों/कर्मचारियों की शिकायतों को आगे की आवश्यक कार्रवाई के लिए प्रशासन के समक्ष प्रस्तुत करने के लिए पंजीकृत करना।', - 'अनुसूचित जातियों, अनुसूचित जनजातियों और अन्य पिछड़े वर्गों के पक्ष में रिक्तियों के आरक्षण और उन्हें स्वीकार्य अन्य लाभों से संबंधित आदेशों और निर्देशों के साथ अधीनस्थ नियुक्ति प्राधिकरणों द्वारा उचित अनुपालन सुनिश्चित करना।' + 'अनुसूचित जातियों, अनुसूचित जनजातियों और अन्य पिछड़े वर्गों के पक्ष में रिक्तियों के आरक्षण और उन्हें स्वीकार्य अन्य लाभों से संबंधित आदेशों और निर्देशों के साथ अधीनस्थ नियुक्ति प्राधिकरणों द्वारा उचित अनुपालन सुनिश्चित करना।', ], - complaint: 'यदि आप औपचारिक शिकायत दर्ज करना चाहते हैं, तो कृपया शिकायत पुस्तिका में फॉर्म भरें, जो अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ, प्रशासनिक भवन, एनआईटी कुरुक्षेत्र में उपलब्ध है। समिति अनुसूचित जाति और अनुसूचित जनजाति के छात्रों, शिक्षकों और कर्मचारियों से प्राप्त भेदभाव की शिकायतों की जांच करेगी और ऐसी शिकायतों का समाधान करेगी।', + complaint: + 'यदि आप औपचारिक शिकायत दर्ज करना चाहते हैं, तो कृपया शिकायत पुस्तिका में फॉर्म भरें, जो अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ, प्रशासनिक भवन, एनआईटी कुरुक्षेत्र में उपलब्ध है। समिति अनुसूचित जाति और अनुसूचित जनजाति के छात्रों, शिक्षकों और कर्मचारियों से प्राप्त भेदभाव की शिकायतों की जांच करेगी और ऐसी शिकायतों का समाधान करेगी।', liaisonOfficerHeading: 'संपर्क अधिकारी', - liaisonOfficer : { + liaisonOfficer: { image: 'fallback/user-image.jpg', name: 'Arun Goel', title: 'प्रोफेसर (विभागाध्यक्ष)', email: 'drarun_goel@yahoo.co.in', - phone: '01744-233349, 01744-233300' + phone: '01744-233349, 01744-233300', }, importantLinksHeading: 'महत्वपूर्ण लिंक', - importantLinks: - [ + importantLinks: [ { title: 'सामाजिक न्याय और अधिकारिता मंत्रालय', - link: 'https://socialjustice.gov.in' + link: 'https://socialjustice.gov.in', }, { title: 'अनुसूचित जातियों की सूची', - link: 'https://socialjustice.gov.in/common/76750' + link: 'https://socialjustice.gov.in/common/76750', }, { title: 'अनुसूचित जनजातियों की सूची', - link: 'https://cdnbbsr.s3waas.gov.in/s301894d6f048493d2cacde3c579c315a3/uploads/2022/03/2022030426.pdf' + link: 'https://cdnbbsr.s3waas.gov.in/s301894d6f048493d2cacde3c579c315a3/uploads/2022/03/2022030426.pdf', }, { title: 'राष्ट्रीय अनुसूचित जाति आयोग, भारत सरकार', - link: 'https://ncsc.nic.in' + link: 'https://ncsc.nic.in', }, { title: 'राष्ट्रीय अनुसूचित जनजाति आयोग, भारत सरकार', - link: 'https://ncstgrams.gov.in' + link: 'https://ncstgrams.gov.in', }, { title: 'अनुसूचित जाति और अनुसूचित जनजाति प्रकोष्ठ एआईसीटीई', - link: 'https://www.aicte.gov.in/bureaus/administration/scst-cell' - } + link: 'https://www.aicte.gov.in/bureaus/administration/scst-cell', + }, ], }, obcpwd: { title: 'अन्य पिछड़ा वर्ग एवं दिव्यांगजन प्रकोष्ठ', - description: - [ - 'एनआईटी कुरुक्षेत्र इस बात के लिए प्रतिबद्ध है कि एक ऐसा कार्य वातावरण स्थापित किया जाए, जहाँ विभिन्न समुदायों से आने वाले छात्र, संकाय सदस्य एवं स्टाफ सद्भावपूर्ण ढंग से कार्य कर सकें। संस्थान का यह पूर्ण प्रयास है कि कार्यस्थल पर किसी भी प्रकार का भेदभाव न हो। जाति-आधारित भेदभाव की किसी भी घटना के संदर्भ में, ओबीसी सेल के लिए नियुक्त लायज़न अधिकारी से संपर्क किया जा सकता है।' - ], - cellFunctionsHeading:'प्रकोष्ठ के कार्य', - cellFunctions: - [ + description: [ + 'एनआईटी कुरुक्षेत्र इस बात के लिए प्रतिबद्ध है कि एक ऐसा कार्य वातावरण स्थापित किया जाए, जहाँ विभिन्न समुदायों से आने वाले छात्र, संकाय सदस्य एवं स्टाफ सद्भावपूर्ण ढंग से कार्य कर सकें। संस्थान का यह पूर्ण प्रयास है कि कार्यस्थल पर किसी भी प्रकार का भेदभाव न हो। जाति-आधारित भेदभाव की किसी भी घटना के संदर्भ में, ओबीसी सेल के लिए नियुक्त लायज़न अधिकारी से संपर्क किया जा सकता है।', + ], + cellFunctionsHeading: 'प्रकोष्ठ के कार्य', + cellFunctions: [ 'आरक्षित वर्गों के कल्याण हेतु छात्रवृत्ति, वजीफे आदि से संबंधित भारत सरकार के एमएचआरडी तथा राज्य सरकार की विभिन्न योजनाओं के उचित क्रियान्वयन को सुनिश्चित करना।', 'शिकायत निवारण: शैक्षणिक, प्रशासनिक या सामाजिक समस्याओं से संबंधित किसी भी शिकायत के लिए। प्रकोष्ठ आवश्यक कार्यवाही करता है तथा समस्या के समाधान हेतु मार्गदर्शन/सहायता प्रदान करता है।', - 'भारत सरकार के एमएचआरडी द्वारा निर्धारित उद्देश्यों और लक्ष्यों की प्राप्ति के लिए आवश्यक अनुवर्ती कार्यवाहियों को अपनाना।' - ], + 'भारत सरकार के एमएचआरडी द्वारा निर्धारित उद्देश्यों और लक्ष्यों की प्राप्ति के लिए आवश्यक अनुवर्ती कार्यवाहियों को अपनाना।', + ], - complaint: 'यदि आप किसी प्रकार की औपचारिक शिकायत दर्ज करना चाहते हैं, तो कृपया शिकायत पुस्तिका में उपलब्ध फॉर्म को भरें, जो एनआईटी कुरुक्षेत्र के प्रशासनिक भवन स्थित ओबीसी प्रकोष्ठ में उपलब्ध है। समिति को प्राप्त ओबीसी छात्र, संकाय सदस्य एवं स्टाफ से संबंधित भेदभाव की शिकायतों की जांच की जाएगी तथा ऐसी शिकायतों का समाधान किया जाएगा।', + complaint: + 'यदि आप किसी प्रकार की औपचारिक शिकायत दर्ज करना चाहते हैं, तो कृपया शिकायत पुस्तिका में उपलब्ध फॉर्म को भरें, जो एनआईटी कुरुक्षेत्र के प्रशासनिक भवन स्थित ओबीसी प्रकोष्ठ में उपलब्ध है। समिति को प्राप्त ओबीसी छात्र, संकाय सदस्य एवं स्टाफ से संबंधित भेदभाव की शिकायतों की जांच की जाएगी तथा ऐसी शिकायतों का समाधान किया जाएगा।', liaisonOfficerHeading: 'संपर्क अधिकारी', - liaisonOfficer : { + liaisonOfficer: { image: 'fallback/user-image.jpg', name: 'Arun Goel', title: 'प्रोफेसर (विभागाध्यक्ष)', email: 'drarun_goel@yahoo.co.in', - phone: '01744-233349, 01744-233300' - } - } + phone: '01744-233349, 01744-233300', + }, + }, }, }, Hostels: { diff --git a/i18n/translations.ts b/i18n/translations.ts index a5522fdc3..b9bb59a9a 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -1,5 +1,7 @@ export async function getTranslations(locale: string): Promise { - return import(`./${locale}.ts`).then((module) => module.default); + return import(`./${locale}.ts`).then( + (module: { default: Translations }) => module.default + ); } export interface Translations { @@ -63,6 +65,7 @@ export interface Translations { examDateSheet: string; timeTable: string; }; + viewMore: string; }; Academics: { notifications: string; @@ -340,8 +343,12 @@ export interface Translations { }; iks: { title: string; - description: string; + description: string[]; iksTeam: string; + coordinators: string; + activitiesPerformed: string; + book: string; + imageGallery: string; }; scst: { title: string; @@ -1088,7 +1095,7 @@ export interface Translations { }; }; DirectorMessage: { - title: String; - message: String[]; + title: string; + message: string[]; }; } From d5aacb6250787e6c36b134523f93f75ad437e7b3 Mon Sep 17 00:00:00 2001 From: Kartik Date: Mon, 5 Jan 2026 02:13:41 +0530 Subject: [PATCH 02/73] UI Additions to Director's page Co-authored-by: Aryawart-kathpal --- app/[locale]/director-message/page.tsx | 64 ------- .../administration/director/director-card.tsx | 84 ++++++++++ .../administration/director/page.tsx | 156 +++++++++++++++++- app/[locale]/page.tsx | 81 +-------- components/heading.tsx | 6 +- components/message-card.tsx | 2 +- i18n/en.ts | 87 ++++++++++ i18n/hi.ts | 88 ++++++++++ i18n/translations.ts | 38 +++++ 9 files changed, 461 insertions(+), 145 deletions(-) delete mode 100644 app/[locale]/director-message/page.tsx create mode 100644 app/[locale]/institute/administration/director/director-card.tsx diff --git a/app/[locale]/director-message/page.tsx b/app/[locale]/director-message/page.tsx deleted file mode 100644 index 725f8201c..000000000 --- a/app/[locale]/director-message/page.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import Heading from '~/components/heading'; -import { getTranslations } from '~/i18n/translations'; -import { getS3Url } from '~/server/s3'; - -export default async function DirectorCorner({ - params: { locale }, -}: { - params: { locale: string }; -}) { - const text = (await getTranslations(locale)).DirectorMessage; - return ( - <> -
- -
-

- {text.message.slice(0, 7).map((message, index) => ( - - {message} - - ))} -

-
-
-

- {text.message.slice(7, 9).map((message, index) => ( - - {message} - - ))} -

-
-
-

- {text.message.slice(9, 11).map((message, index) => ( - - {message} - - ))} -

-
-
-

- {text.message[11]} -

-
-
-

- {text.message[12]} -

-
-
- - ); -} diff --git a/app/[locale]/institute/administration/director/director-card.tsx b/app/[locale]/institute/administration/director/director-card.tsx new file mode 100644 index 000000000..765b8bbb1 --- /dev/null +++ b/app/[locale]/institute/administration/director/director-card.tsx @@ -0,0 +1,84 @@ +import Image from 'next/image'; + +import { cn } from '~/lib/utils'; + +export default function DirectorCard({ + className, + image, + name, + position, + phone, + fax, + mobile, + email, + labels, +}: { + className?: string; + image: string; + name: string; + position: string; + phone: string; + fax: string; + mobile: string; + email: string; + labels: { + phoneNo: string; + faxNo: string; + mobileNo: string; + emailId: string; + }; +}) { + return ( +
+ {/* Director Image */} +
+ {name} +
+ + {/* Director Info */} +
+

+ {name} +

+

+ {position} +

+ +
    +
  • + {labels.phoneNo}{' '} + {phone} +
  • +
  • + {labels.faxNo}{' '} + {fax} +
  • +
  • + {labels.mobileNo}{' '} + {mobile} +
  • +
  • + {labels.emailId}{' '} + + {email} + +
  • +
+
+
+ ); +} diff --git a/app/[locale]/institute/administration/director/page.tsx b/app/[locale]/institute/administration/director/page.tsx index 30d9eeed5..9e90a4e6b 100644 --- a/app/[locale]/institute/administration/director/page.tsx +++ b/app/[locale]/institute/administration/director/page.tsx @@ -1,9 +1,159 @@ -import { WorkInProgressStatus } from '~/components/status'; +import Image from 'next/image'; +import Link from 'next/link'; +import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; -export default function Director({ +import Heading from '~/components/heading'; +import { getTranslations } from '~/i18n/translations'; +import ImageHeader from '~/components/image-header'; + +import DirectorCard from './director-card'; + +export default async function DirectorCorner({ params: { locale }, }: { params: { locale: string }; }) { - return ; + const text = (await getTranslations(locale)).DirectorPage; + + return ( + <> + {/* ---------- HEADER ---------- */} + + + {/* ---------- DIRECTOR’S PROFILE ---------- */} +
+ + +
+ + {/* ---------- BRIEF CV OF DIRECTOR ---------- */} +
+ + {text.cv.map((item, index) => ( +

+ {item} +

+ ))} +
+ + {/* ---------- DIRECTOR’S MESSAGE ---------- */} +
+ + {text.DirectorMessage.map((msg, index) => ( +

{msg}

+ ))} +
+ + {/* ---------- DIRECTOR’S OFFICE / EMPLOYEES ---------- */} +
+ +
    + {text.employes.map((employe, index) => ( +
  • + {employe.name} +
    +
    +

    + {employe.name} +

    + + {employe.position} + +
    +
    + + + + {employe.email} + + + + + + {employe.phone} + + +
    +
    +
  • + ))} +
+
+ + {/* ---------- PREVIOUS DIRECTORS ---------- */} +
+ + {text.preDirectors.map((director, index) => ( +
+ +
+ ))} +
+ + ); } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 7c6051236..a764ee5c5 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,13 +1,6 @@ import Image from 'next/image'; -import { BsCalendar3, BsClockFill } from 'react-icons/bs'; -import { - FaFacebook, - FaGraduationCap, - FaInstagram, - FaLinkedinIn, -} from 'react-icons/fa'; -import { FaXTwitter } from 'react-icons/fa6'; -import { MdDateRange } from 'react-icons/md'; +import { BsLinkedin } from 'react-icons/bs'; +import { MdEmail, MdPhone } from 'react-icons/md'; import Notifications from '~/app/notifications'; import { Button } from '~/components/buttons'; @@ -21,7 +14,6 @@ import { import Heading from '~/components/heading'; import MessageCard from '~/components/message-card'; import { getTranslations } from '~/i18n/translations'; -import { cn } from '~/lib/utils'; import { type events, type notifications } from '~/server/db'; import Events from './events'; @@ -76,8 +68,8 @@ export default async function Home({ /> {title && ( -
-
+
+

{title}

@@ -100,24 +92,14 @@ export default async function Home({
{[ - { - href: 'https://www.facebook.com/nitkurukshetraofficialpage', - icon: FaFacebook, - }, - { - href: 'https://www.instagram.com/nitkurukshetra', - icon: FaInstagram, - }, - { - href: 'https://twitter.com/NITKURUKSHETRA', - icon: FaXTwitter, - }, + { href: 'mailto:director@nitkkr.ac.in', icon: MdEmail }, + { href: 'tel:+911742238570', icon: MdPhone }, { href: 'https://www.linkedin.com/school/national-institute-of-technology-kurukshetra-haryana', - icon: FaLinkedinIn, + icon: BsLinkedin, }, ].map(({ href, icon: Icon }, index) => ( - +
- -
- -
); } diff --git a/components/heading.tsx b/components/heading.tsx index 5627188e2..6c1d3e262 100644 --- a/components/heading.tsx +++ b/components/heading.tsx @@ -91,11 +91,7 @@ export default function Heading({ return (
diff --git a/components/message-card.tsx b/components/message-card.tsx index 84ceeb9fd..d18604337 100644 --- a/components/message-card.tsx +++ b/components/message-card.tsx @@ -93,4 +93,4 @@ export default function MessageCard({ )}
); -} +} \ No newline at end of file diff --git a/i18n/en.ts b/i18n/en.ts index 0c9721d18..9c0ae39b4 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -1859,6 +1859,93 @@ rolls down to 60% of the eligible students for second round of placement session ], }, }, + DirectorPage: { + pageTitle: 'DIRECTOR', + sections: [ + 'Director’s Profile', + 'Brief CV Of Director', + 'Director’s Message', + 'Director’s Office', + 'Previous Directors', + ], + labels: { + phoneNo: 'Phone No.:', + faxNo: 'Fax No.:', + mobileNo: 'Mobile No.:', + emailId: 'Email-ID:', + }, + Director: { + name: 'Professor B.V. Ramana Reddy', + position: 'Director, National Institute of Technology, Kurukshetra', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + title: [ + 'DIRECTOR’S PROFILE', + 'BRIEF CV OF DIRECTOR', + 'DIRECTOR’S MESSAGE', + 'DIRECTOR’S OFFICE', + 'PREVIOUS DIRECTORS', + ], + cv: [ + ' He took over as Director, National Institute of Technology Kurukshetra on 05th February, 2022 (Basant Panchmi). He is an alumnus of Andhra University, IIT Roorkee and NIT Kurukshetra. In his long career spanning over 35 years, he served in various capacities as teaching faculty at national institutes of repute at NIT Kurukshetra (during 1991- 95), NIT Hamirpur (1995-1999) known earlier as REC. He also served as Assoc. Prof. & as Professor at University School of Information & Communication Technoloy (USICT), GGSIP University New Delhi for the last 22 years (2000-2022).', + 'His current research interests include Wireless communications which include mobile, Adhoc and sensor based networks, computer communication networks, Semiconductor and VLSI circuits and microwave & optical communications. He has more than 100 publications in International, National journals and International Conferences to his credit. He produced Thirteen (13) Ph.D.s, and currently supervising Eight (8) PhD scholars.', + 'Besides, he is a Fellow of IETE, IE, ISTE and a member of other professional bodies such as IEEE, CSI and SEMCEI. He is an active member in various committees constituted by AICTE, UGC, NAAC, TEQIP and NIC. He visited foreign Universities situated in Singapore and China.', + 'He is currently actively participating in educational reforms and value based education, in tune with National Skill Qualification Framework (NSQF) and contributed to vocational education in the country. He strongly believes in holistic development and growth of the next generation children, and dreams of a society where each and every species living on Earth lives harmoniously (VASUDEVA KUTUMBAKAM). Further, he strongly believes in to see glitter in the eyes of his subjects as an award, reward or recognition.', + 'His vision for NIT KKR: He is focused on implementing NEP 2020 in toto at NIT Kurukshetra. He further wants to change the curriculum from outcome based education model into value based education model from the coming academic session 2022-23. The intent is to transform NIT KKR as Takshashila of yesteryears and bringing back India as Vishva Guru and put NIT KKR at the World map as leading educational institute offering holistic personalities to the World and produce leaders from NIT Kurukshetra. We have entered into 60th year of our existence and are upbeat in going for a yearlong celebration.', + ], + DirectorMessage: [ + 'India, the land of seekers, is at the cusp of becoming Vishwa Guru all over again after 1100 years of subjugation, wars, annexures and humiliation. It is again a free country due to the sacrifices made by our leaders, freedom fighters and has learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures, languages all over again since the last 75 years. Unity in Diversity is our mantra while making our nation stronger in every sphere.', + 'India, the land of seekers, is at the cusp of becoming Vishwa Guru all over again after 1100 years of subjugation, wars, annexures and humiliation. It is again a free country due to the sacrifices made by our leaders, freedom fighters and has learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures, languages all over again since the last 75 years. Unity in Diversity is our mantra while making our nation stronger in every sphere.', + ], + employes: [ + { + name: 'Arun Goel', + image: 'fallback/user-image.jpg', + position: 'Head of Department, CSE', + phone: '+91-1744-233208', + email: 'director@nitkkr.ac.in', + }, + { + name: 'Arun Goel', + image: 'fallback/user-image.jpg', + position: 'Head of Department, CSE', + phone: '+91-1744-233208', + email: 'director@nitkkr.ac.in', + }, + ], + preDirectors: [ + { + name: 'Professor B.V. Ramana Reddy', + image: 'assets/director.jpeg', + position: 'Former Director, NIT Kurukshetra -2022-2025', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + { + name: 'Professor B.V. Ramana Reddy', + image: 'assets/director.jpeg', + position: 'Former Director, NIT Kurukshetra -2022-2025', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + { + name: 'Professor B.V. Ramana Reddy', + image: 'assets/director.jpeg', + position: 'Former Director, NIT Kurukshetra -2022-2025', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + ], + }, }; export default text; diff --git a/i18n/hi.ts b/i18n/hi.ts index ec9ca63aa..76f46058e 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -1826,6 +1826,94 @@ const text: Translations = { ], }, }, + DirectorPage: { + pageTitle: 'निदेशक', + sections: [ + 'निदेशक की प्रोफ़ाइल', + 'निदेशक का संक्षिप्त परिचय', + 'निदेशक का संदेश', + 'निदेशक का कार्यालय', + 'पूर्व निदेशक', + ], + labels: { + phoneNo: 'फ़ोन नं.:', + faxNo: 'फ़ैक्स नं.:', + mobileNo: 'मोबाइल नं.:', + emailId: 'ईमेल-आईडी:', + }, + Director: { + name: 'प्रोफेसर बी. वी. रामना रेड्डी', + position: 'निदेशक, राष्ट्रीय प्रौद्योगिकी संस्थान, कुरुक्षेत्र', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + + title: [ + 'निदेशक की प्रोफ़ाइल', + 'निदेशक का संक्षिप्त जीवनवृत्त (सीवी)', + 'निदेशक का संदेश', + 'निदेशक का कार्यालय', + 'पूर्व निदेशकगण', + ], + cv: [ + 'उन्होंने 05 फरवरी 2022 (बसंत पंचमी) को राष्ट्रीय प्रौद्योगिकी संस्थान, कुरुक्षेत्र के निदेशक के रूप में कार्यभार ग्रहण किया। वे आंध्र विश्वविद्यालय, आईआईटी रुड़की और एनआईटी कुरुक्षेत्र के पूर्व छात्र हैं। अपने 35 वर्षों से अधिक लंबे करियर में उन्होंने विभिन्न भूमिकाओं में देश के प्रतिष्ठित राष्ट्रीय संस्थानों में अध्यापन कार्य किया है — एनआईटी कुरुक्षेत्र (1991–1995) और एनआईटी हमीरपुर (1995–1999, जिसे पहले आरईसी के नाम से जाना जाता था)। उन्होंने 22 वर्षों (2000–2022) तक गुरु गोबिंद सिंह इंद्रप्रस्थ विश्वविद्यालय, नई दिल्ली के सूचना एवं संचार प्रौद्योगिकी विद्यालय (USICT) में सहयोगी प्रोफेसर और प्रोफेसर के रूप में भी कार्य किया।', + 'उनका वर्तमान शोध क्षेत्र वायरलेस संचार है, जिसमें मोबाइल, एडहॉक और सेंसर आधारित नेटवर्क, कंप्यूटर संचार नेटवर्क, सेमीकंडक्टर और वीएलएसआई सर्किट, माइक्रोवेव और ऑप्टिकल संचार शामिल हैं। उन्होंने 100 से अधिक अंतरराष्ट्रीय एवं राष्ट्रीय शोध पत्र और सम्मेलन प्रस्तुतियाँ प्रकाशित की हैं। उन्होंने 13 पीएचडी विद्यार्थियों को मार्गदर्शन दिया है और वर्तमान में 8 शोधार्थियों का मार्गदर्शन कर रहे हैं।', + 'इसके अतिरिक्त, वे IETE, IE, ISTE के फेलो हैं और IEEE, CSI तथा SEMCEI जैसी अन्य पेशेवर संस्थाओं के सदस्य भी हैं। उन्होंने AICTE, UGC, NAAC, TEQIP और NIC द्वारा गठित विभिन्न समितियों में सक्रिय भूमिका निभाई है। उन्होंने सिंगापुर और चीन स्थित विदेशी विश्वविद्यालयों का भी दौरा किया है।', + 'वर्तमान में वे राष्ट्रीय कौशल योग्यता रूपरेखा (NSQF) के अनुरूप शिक्षा सुधार और मूल्य-आधारित शिक्षा में सक्रिय रूप से भाग ले रहे हैं तथा देश में व्यावसायिक शिक्षा में योगदान दे रहे हैं। वे समग्र विकास में विश्वास रखते हैं और ऐसी समाज की परिकल्पना करते हैं जहाँ पृथ्वी पर हर जीव सामंजस्यपूर्वक जीवन व्यतीत करे (वसुधैव कुटुम्बकम)। वे अपने विद्यार्थियों की आँखों में चमक को ही अपना पुरस्कार और सम्मान मानते हैं।', + 'एनआईटी कुरुक्षेत्र के लिए उनका दृष्टिकोण: वे संस्थान में राष्ट्रीय शिक्षा नीति (NEP 2020) को पूर्ण रूप से लागू करने पर केंद्रित हैं। वे 2022-23 के शैक्षणिक सत्र से पाठ्यक्रम को आउटपुट आधारित शिक्षा से मूल्य आधारित शिक्षा मॉडल में बदलने के इच्छुक हैं। उनका उद्देश्य एनआईटी कुरुक्षेत्र को तक्षशिला की भांति विश्व प्रसिद्ध संस्थान बनाना है जो वैश्विक स्तर पर समग्र व्यक्तित्व विकसित कर समाज के लिए नेतृत्वकर्ता तैयार करे। एनआईटी कुरुक्षेत्र अपनी स्थापना के 60वें वर्ष में प्रवेश कर चुका है और वर्षभर चलने वाले उत्सवों की तैयारी में है।', + ], + DirectorMessage: [ + "भारत, साधकों की भूमि, 1100 वर्षों की गुलामी, युद्धों, अधिग्रहणों और अपमान के बाद फिर से विश्वगुरु बनने की दहलीज पर खड़ा है। यह देश हमारे नेताओं, स्वतंत्रता सेनानियों के बलिदानों के कारण पुनः स्वतंत्र हुआ है और पिछले 75 वर्षों में अपनी विविधता, संस्कृतियों, भाषाओं के साथ एक सशक्त राष्ट्र के रूप में उभरने की कला सीख चुका है। 'विविधता में एकता' हमारा मूल मंत्र है जो हमें हर क्षेत्र में सशक्त बना रहा है।", + "भारत, साधकों की भूमि, 1100 वर्षों की गुलामी, युद्धों, अधिग्रहणों और अपमान के बाद फिर से विश्वगुरु बनने की दहलीज पर खड़ा है। यह देश हमारे नेताओं, स्वतंत्रता सेनानियों के बलिदानों के कारण पुनः स्वतंत्र हुआ है और पिछले 75 वर्षों में अपनी विविधता, संस्कृतियों, भाषाओं के साथ एक सशक्त राष्ट्र के रूप में उभरने की कला सीख चुका है। 'विविधता में एकता' हमारा मूल मंत्र है जो हमें हर क्षेत्र में सशक्त बना रहा है।", + ], + employes: [ + { + name: 'अरुण गोयल', + image: 'director/Arun.jpg', + position: 'विभागाध्यक्ष, कंप्यूटर विज्ञान एवं अभियांत्रिकी', + phone: '+91-1744-233208', + email: 'director@nitkkr.ac.in', + }, + { + name: 'अरुण गोयल', + image: 'director/Arun.jpg', + position: 'विभागाध्यक्ष, कंप्यूटर विज्ञान एवं अभियांत्रिकी', + phone: '+91-1744-233208', + email: 'director@nitkkr.ac.in', + }, + ], + preDirectors: [ + { + name: 'प्रोफेसर बी. वी. रामना रेड्डी', + image: 'assets/director.jpeg', + position: 'पूर्व निदेशक, एनआईटी कुरुक्षेत्र (2022–2025)', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + { + name: 'प्रोफेसर बी. वी. रामना रेड्डी', + image: 'assets/director.jpeg', + position: 'पूर्व निदेशक, एनआईटी कुरुक्षेत्र (2022–2025)', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + { + name: 'प्रोफेसर बी. वी. रामना रेड्डी', + image: 'assets/director.jpeg', + position: 'पूर्व निदेशक, एनआईटी कुरुक्षेत्र (2022–2025)', + phone: '+91-1744-233208', + fax: '+91-1744-238050', + mobile: '+91-9876543210', + email: 'director@nitkkr.ac.in', + }, + ], + }, }; export default text; diff --git a/i18n/translations.ts b/i18n/translations.ts index b9bb59a9a..58fda46ac 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -1098,4 +1098,42 @@ export interface Translations { title: string; message: string[]; }; + DirectorPage: { + pageTitle: string; + sections: string[]; + labels: { + phoneNo: string; + faxNo: string; + mobileNo: string; + emailId: string; + }; + Director: { + name: string; + position: string; + phone: string; + fax: string; + mobile: string; + email: string; + }; + cv: string[]; + title: string[]; + DirectorMessage: string[]; + employes: { + name: string; + position: string; + image: string; + phone: string; + email: string; + }[]; + + preDirectors: { + name: string; + position: string; + image: string; + phone: string; + fax: string; + mobile: string; + email: string; + }[]; + }; } From b459d1241967f4b170c4aa982e7e0a1723fd33f2 Mon Sep 17 00:00:00 2001 From: Arnav Sharma <145358467+ArnavSharma005@users.noreply.github.com> Date: Fri, 9 Jan 2026 19:37:48 +0530 Subject: [PATCH 03/73] Dev/mou (#424) This pull request introduces several improvements and refactors across multiple pages, focusing on dynamic data fetching from the database, UI consistency, and code style cleanups. The most significant changes are the migration of static research data to dynamic database queries, UI/UX refinements for the SC/ST cell page, and minor formatting and code cleanup in other components. **Dynamic Data Fetching and Table Updates:** * Migrated static data for Memorandum of Understanding (MoU) and Sponsored Research Projects in `app/[locale]/research/page.tsx` to fetch directly from the database, including transforming and formatting the data for table display. This includes updating the table headers and rows to show new fields such as sanctioned file/order number, date, and project status. ([app/[locale]/research/page.tsxR24-R38](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67R24-R38), [app/[locale]/research/page.tsxR98-R132](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67R98-R132), [app/[locale]/research/page.tsxL210-R148](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67L210-R148), [app/[locale]/research/page.tsxL473-R411](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67L473-R411), [app/[locale]/research/page.tsxR449-R453](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67R449-R453), [app/[locale]/research/page.tsxR740-R742](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67R740-R742), [app/[locale]/research/page.tsxR761-R763](diffhunk://#diff-7e2ef4caf94dbc72306307e50144de2644725209044410ff5dc769152068eb67R761-R763)) **UI/UX and Styling Improvements:** * Refined layout, className ordering, and responsive styles for the SC/ST cell page (`app/[locale]/institute/cells/scst/page.tsx`), including adjustments to faculty info display, section spacing, and list styling for better readability and consistency. ([app/[locale]/institute/cells/scst/page.tsxL18-R18](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL18-R18), [app/[locale]/institute/cells/scst/page.tsxL33-R40](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL33-R40), [app/[locale]/institute/cells/scst/page.tsxL58-R65](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL58-R65), [app/[locale]/institute/cells/scst/page.tsxL86-R139](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL86-R139), [app/[locale]/institute/cells/scst/page.tsxL159-R163](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL159-R163), [app/[locale]/institute/cells/scst/page.tsxL179-L190](diffhunk://#diff-19c9706882ffe3c5011d10c9fc1e574aa3f0c8095b33184bddde8ec8abf2ba6cL179-L190)) * Minor UI adjustments in the gallery component for improved code readability and style formatting. [[1]](diffhunk://#diff-7a11d6e30b52d89cba0ae61febfb2a8dc1c64a312509fdbeffa3119de9061aa7L188-R191) [[2]](diffhunk://#diff-7a11d6e30b52d89cba0ae61febfb2a8dc1c64a312509fdbeffa3119de9061aa7L204-R211) **Code Style and Formatting Cleanups:** * Added or corrected trailing commas and semicolons for consistency in various files. ([app/[locale]/academics/curricula/page.tsxL123-R123](diffhunk://#diff-f28e90cdfc2124ecddf73b3a9d4522b6270d42112fa9b8a81c67656ed7722c86L123-R123), [app/[locale]/header.tsxL88-R88](diffhunk://#diff-05aeabaff5d1ec0d925bc6519f8dcf6cdc4f94efcf74117ecca3fbbd77eb7777L88-R88)) * Removed unnecessary blank lines and improved code formatting for clarity. ([app/[locale]/student-activities/clubs/[display_name]/event-section.tsxL96](diffhunk://#diff-fe0a45bba40a113ff0cfe178c0a454d4980293098763ae248037759f44e58de0L96)) These changes collectively enhance maintainability, data accuracy, and user experience across the affected pages. --------- Co-authored-by: soumil221 --- app/[locale]/research/page.tsx | 159 ++++++------------ i18n/en.ts | 7 +- i18n/hi.ts | 17 +- i18n/translations.ts | 3 + server/db/populate/index.ts | 0 server/db/schema/index.ts | 3 + server/db/schema/memorandum.schema.ts | 7 + ...ored-research-projects-faculties.schema.ts | 18 ++ .../sponsored-research-projects.schema.ts | 42 +++++ 9 files changed, 141 insertions(+), 115 deletions(-) create mode 100644 server/db/populate/index.ts create mode 100644 server/db/schema/memorandum.schema.ts create mode 100644 server/db/schema/sponsored-research-projects-faculties.schema.ts create mode 100644 server/db/schema/sponsored-research-projects.schema.ts diff --git a/app/[locale]/research/page.tsx b/app/[locale]/research/page.tsx index 42c030f42..a3af21e1e 100644 --- a/app/[locale]/research/page.tsx +++ b/app/[locale]/research/page.tsx @@ -24,13 +24,21 @@ import { db } from '~/server/db'; import type { copyrights, designs, + mous, patents, researchAndConsultancy, + sponsoredResearchProjects, + sponsoredResearchProjectsFaculties, } from '~/server/db/schema'; + type PatentsTable = typeof patents.$inferSelect; type CopyrightsTable = typeof copyrights.$inferSelect; type DesignsTable = typeof designs.$inferSelect; type ResearchAndConsultancyTable = typeof researchAndConsultancy.$inferSelect; +type Moustable = typeof mous.$inferSelect; +// type SponsoredProjectsTable = typeof sponsoredResearchProjects.$inferSelect; +// type SponsoredProjectsFacultiesTable = typeof sponsoredResearchProjectsFaculties.$inferSelect; + export default async function PatentsAndTechnology({ params: { locale }, @@ -90,111 +98,41 @@ export default async function PatentsAndTechnology({ orderBy: (rc) => sql`SUBSTRING(${rc.year}, 1, 4)::integer DESC`, // sorting by year to latest } ); + const mous = await db.query.mous.findMany(); + const staticMemorandum: Moustable[] = mous; + const formattedMemorandum = staticMemorandum.map((item) => { + return { + organization: item.organization, + date: item.signingDate, + }; + }); - const staticMemorandum = [ - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - { - organization: 'CSIR-Central Road Research Institute, New Delhi', - date: '10-10-2023', - }, - ]; - const staticProjects = [ - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', + const projects= await db.query.sponsoredResearchProjects.findMany({ + with: { + faculties: true, + department: true, // Make sure to also include department relation }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - { - year: '2014-17', - department: 'chemistry', - facultyName: 'Dr. Amilan Jose D', - title: - 'Supermolecular fluorescent probes for the selective detection of biological signaling molecule (H2S) and real time assay', - agency: 'hihi', - amount: '69', - }, - ]; + }); + + + // Transform projects data into the format needed for the table + const staticProjects = projects.map((project) => { + const facultyNames = project.faculties && project.faculties.length > 0 + ? project.faculties.map((faculty) => faculty.facultyName).join(', ') + : 'N/A'; + + return { + year: project.year, + department: project.department?.name ?? 'N/A', + facultyName: facultyNames, + title: project.titleOfProject, + agency: project.agency, + amount: project.amountInLakh, + sanctionedFileOrderNo: project.sanctionedFileOrderNO ?? 'N/A', + sanctionedDate: project.sanctionedDate ?? 'N/A', + status: project.status ?? 'N/A', + }; + }); const base = getS3Url(); // Get the total count for pagination const getResearchCount = async () => { @@ -210,7 +148,7 @@ export default async function PatentsAndTechnology({ return [{ count }]; }; const getMemorandumCount = async () => { - const count = staticMemorandum.length; // Replace with your actual DB call + const count = formattedMemorandum.length; // Replace with your actual DB call return [{ count }]; }; @@ -473,7 +411,7 @@ export default async function PatentsAndTechnology({ } > @@ -511,8 +449,11 @@ export default async function PatentsAndTechnology({ text.projects.title, text.projects.agency, text.projects.amount, + text.projects.sanctionedFileOrderNo, + text.projects.sanctionedDate, + text.projects.status, ].map((headerText, index) => ( - {headerText} + {headerText} ))} @@ -799,6 +740,9 @@ const ProjectsTable = ({ title: string; agency: string; amount: string; + sanctionedFileOrderNo: string; + sanctionedDate: string; + status: string; }[]; currentPage: number; itemsPerPage?: number; @@ -817,6 +761,9 @@ const ProjectsTable = ({ item.title, item.agency, item.amount, + item.sanctionedFileOrderNo, + item.sanctionedDate, + item.status, ]; return ( diff --git a/i18n/en.ts b/i18n/en.ts index 9c0ae39b4..31e4bd607 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -1683,9 +1683,12 @@ Centre/empanelled hospital/Govt. hospital after giving preliminary treatment.`, year: 'Year', department: 'Department', facultyName: 'Faculty Name', - title: 'Title', + title: 'Title of Project', agency: 'Agency', - amount: 'Amount Sanctioned (in Rs.)', + amount: 'Amount (Rs.) in lakh', + sanctionedFileOrderNo: 'Sanctioned File/Order No.', + sanctionedDate: 'Sanctioned Date', + status: 'Status', }, archive: { title: 'Archive', diff --git a/i18n/hi.ts b/i18n/hi.ts index 76f46058e..a1f8ffcc0 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -1655,13 +1655,16 @@ const text: Translations = { signingDate: 'हस्ताक्षर तिथि', }, projects: { - number: 'क्रम संख्या', - year: 'वर्ष', - department: 'विभाग', - facultyName: 'संकाय का नाम', - title: 'शीर्षक', - agency: 'एजेंसी', - amount: 'स्वीकृत राशि (रु. में)', + number: 'क्रम संख्या', + year: 'वर्ष', + department: 'विभाग', + facultyName: 'संकाय का नाम', + title: 'परियोजना का शीर्षक', + agency: 'एजेंसी', + amount: 'राशि (रु.) लाख में', + sanctionedFileOrderNo: 'स्वीकृत फाइल/आदेश संख्या', + sanctionedDate: 'स्वीकृत तिथि', + status: 'स्थिति', }, archive: { title: 'संग्रह', diff --git a/i18n/translations.ts b/i18n/translations.ts index 58fda46ac..998bc615c 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -1014,6 +1014,9 @@ export interface Translations { title: string; agency: string; amount: string; + sanctionedFileOrderNo: string; + sanctionedDate: string; + status: string; }; archive: { title: string; diff --git a/server/db/populate/index.ts b/server/db/populate/index.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index 1213fc336..1fbe1eb5d 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -32,3 +32,6 @@ export * from './sections.schema'; export * from './staff.schema'; export * from './student-academic-details.schema'; export * from './students.schema'; +export * from './sponsored-research-projects.schema'; +export * from './sponsored-research-projects-faculties.schema'; +export * from './memorandum.schema'; diff --git a/server/db/schema/memorandum.schema.ts b/server/db/schema/memorandum.schema.ts new file mode 100644 index 000000000..5a2ae358b --- /dev/null +++ b/server/db/schema/memorandum.schema.ts @@ -0,0 +1,7 @@ +import { pgTable } from 'drizzle-orm/pg-core'; + +export const mous = pgTable('mous', (t) => ({ + id: t.serial('id').primaryKey(), + organization: t.varchar('organization').notNull(), + signingDate: t.date('signingDate').notNull(), +})); diff --git a/server/db/schema/sponsored-research-projects-faculties.schema.ts b/server/db/schema/sponsored-research-projects-faculties.schema.ts new file mode 100644 index 000000000..c216468bd --- /dev/null +++ b/server/db/schema/sponsored-research-projects-faculties.schema.ts @@ -0,0 +1,18 @@ +import {pgTable} from "drizzle-orm/pg-core"; +import { sponsoredResearchProjects } from "./sponsored-research-projects.schema"; +import { relations } from "drizzle-orm"; +export const sponsoredResearchProjectsFaculties = pgTable('sponsored_research_projects_faculties', (t) => ({ + id: t.serial('id').primaryKey(), + sponsoredResearchProjectId: t.integer('sponsored_research_project_id').references(() => sponsoredResearchProjects.id).notNull(), + facultyName: t.varchar('faculty_name').notNull(), +})); + +export const sponsoredResearchProjectsFacultiesRelations = relations( + sponsoredResearchProjectsFaculties, + ({ one }) => ({ + sponsoredResearchProject: one(sponsoredResearchProjects, { + fields: [sponsoredResearchProjectsFaculties.sponsoredResearchProjectId], + references: [sponsoredResearchProjects.id], + }), + }) +); \ No newline at end of file diff --git a/server/db/schema/sponsored-research-projects.schema.ts b/server/db/schema/sponsored-research-projects.schema.ts new file mode 100644 index 000000000..83560d765 --- /dev/null +++ b/server/db/schema/sponsored-research-projects.schema.ts @@ -0,0 +1,42 @@ +import { sql } from "drizzle-orm"; +import { pgTable } from "drizzle-orm/pg-core"; +import { relations } from "drizzle-orm"; +import { departments } from "./departments.schema"; +import { sponsoredResearchProjectsFaculties } from "./sponsored-research-projects-faculties.schema"; + +export const sponsoredResearchProjects = pgTable( + "sponsored_research_projects", + (t) => ({ + id: t.serial("id").primaryKey(), + year: t.varchar("year").notNull(), + departmentId: t + .smallserial("department_id") + .references(() => departments.id) + .notNull(), + titleOfProject: t.varchar("title_of_project").notNull(), + agency: t.varchar("agency").notNull(), + amountInLakh: t + .numeric("amount_in_lakh", { precision: 10, scale: 2 }) + .notNull(), + sanctionedFileOrderNO: t.varchar("sanctioned_file_order_no"), + sanctionedDate: t.date("sanctioned_date"), + status: t + .varchar("status", { enum: ["ongoing", "completed"] }) + .default("ongoing") + .notNull(), + }), + (t) => ({ + validYearFormat: sql`CHECK (${t.year} ~ '^[0-9]{4}-[0-9]{2}$')`, + }) +); + +export const sponsoredResearchProjectsRelations = relations( + sponsoredResearchProjects, + ({ one, many }) => ({ + department: one(departments, { + fields: [sponsoredResearchProjects.departmentId], + references: [departments.id], + }), + faculties: many(sponsoredResearchProjectsFaculties), + }) +); From 96c819fe88b10a320ea1cc184a8be0ccd9c49c67 Mon Sep 17 00:00:00 2001 From: Aryawart-kathpal Date: Mon, 12 Jan 2026 13:50:41 +0530 Subject: [PATCH 04/73] fix: Fixed the link of alumni cell --- app/[locale]/header.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/[locale]/header.tsx b/app/[locale]/header.tsx index f84594f64..3a43f33cc 100644 --- a/app/[locale]/header.tsx +++ b/app/[locale]/header.tsx @@ -122,7 +122,7 @@ export default async function Header({ locale }: { locale: string }) { { label: text.activities, href: 'student-activities' }, { label: text.alumni, - href: 'https://nitkkraa.org', + href: 'https://alumni.nitkkr.ac.in/', isExternal: true, }, { From d62c0480475ac0c2ced10fc5fd22a1f63e248eed Mon Sep 17 00:00:00 2001 From: Debatreya Das <116421305+Debatreya@users.noreply.github.com> Date: Mon, 12 Jan 2026 14:05:25 +0530 Subject: [PATCH 05/73] Refactor/faculty (#419) A constant Faculty updates to Staged --------- Co-authored-by: Navneet Kaur Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: ArnavSharma005 <145358467+ArnavSharma005@users.noreply.github.com> Co-authored-by: heydoyouknowme0 --- .../faculty-and-staff/client-components.tsx | 350 +++++++++++++++++- app/[locale]/faculty-and-staff/page.tsx | 201 +++++----- app/[locale]/faculty-and-staff/utils.tsx | 6 +- 3 files changed, 464 insertions(+), 93 deletions(-) diff --git a/app/[locale]/faculty-and-staff/client-components.tsx b/app/[locale]/faculty-and-staff/client-components.tsx index c7b5d029a..8ae886e20 100644 --- a/app/[locale]/faculty-and-staff/client-components.tsx +++ b/app/[locale]/faculty-and-staff/client-components.tsx @@ -1,6 +1,162 @@ 'use client'; -import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { createPortal } from 'react-dom'; +import React, { Suspense, useEffect, useMemo, useRef, useState } from 'react'; import Link from 'next/link'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { FaTimes } from 'react-icons/fa'; +import { MdFilterList } from 'react-icons/md'; + +import Loading from '~/components/loading'; +import { ScrollArea } from '~/components/ui/scroll-area'; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from '~/components/inputs'; +import { cn } from '~/lib/utils'; + +interface Dept { + id: number; + name: string; + urlName: string; +} +export function MobileFilters({ + departments, + department, + className, +}: { + departments?: Dept[]; + department?: string | string[]; + className?: string; +}) { + const [open, setOpen] = useState(false); + const panelRef = useRef(null); + + // Lock body scroll while open + useEffect(() => { + const prev = document.body.style.overflow; + if (open) document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prev; + }; + }, [open]); + + // Close on outside click + useEffect(() => { + if (!open) return; + + document.body.classList.add('overflow-hidden'); + return () => { + document.body.classList.remove('overflow-hidden'); + }; + }, [open]); + + + return ( +
+ {/* Filter button */} + + + {/* Backdrop */} +
setOpen(false)} + > + +
+
+ {open && createPortal( + , + document.body + )} +
+ ); +} export function ClearFiltersButton() { const router = useRouter(); @@ -56,3 +212,195 @@ export function PreserveParamsLink({ ); } + +interface Dept { + id: number; + name: string; + urlName: string; +} + +export function DepartmentsClient({ + departments, + department, + select = false, +}: { + departments: Dept[]; + department?: string | string[]; + select?: boolean; +}) { + const [showAll, setShowAll] = useState(false); + const optionsToShow = 4; + + const selectedDepartments = useMemo( + () => + Array.isArray(department) ? department : department ? [department] : [], + [department] + ); + const sortedDepartments = useMemo(() => { + return [...departments].sort((a, b) => { + const aSelected = selectedDepartments.includes(a.urlName); + const bSelected = selectedDepartments.includes(b.urlName); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + }, [departments, selectedDepartments]); + + const getUpdatedDepartments = (urlName: string) => + selectedDepartments.includes(urlName) + ? selectedDepartments.filter((d) => d !== urlName) + : [...selectedDepartments, urlName]; + + if (select) { + return ( + + + {name} + +
+ ))} + + + ); + } + + // Desktop/list mode: show limited items and "View more" button that toggles full list. + const visible = showAll ? sortedDepartments : sortedDepartments.slice(0, optionsToShow); + + return ( +
+
    + {/* Constrain the list height and allow internal scrolling */} +
    + {visible.map(({ name, urlName }, index) => ( +
  1. + +
    +
    + +
    + {name} +
    +
    +
  2. + ))} +
    +
+ + {sortedDepartments.length > optionsToShow && ( +
+ +
+ )} +
+ ); +} + +/** + * Small client-side Designations UI for mobile carousel. + */ +function DesignationsClient() { + const searchParams = useSearchParams(); + const selected = searchParams?.getAll('designation') ?? []; + const options = ['faculty', 'staff']; + + const sortedOptions = useMemo(() => { + return [...options].sort((a, b) => { + const aSelected = selected.includes(a); + const bSelected = selected.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + }, [selected]); + + + const getUpdatedDesignations = (option: string) => + selected.includes(option) + ? selected.filter((d) => d !== option) + : [...selected, option]; + + return ( +
    + {sortedOptions.map((option, index) => ( +
  1. + +
    +
    + +
    + + {option.charAt(0).toUpperCase() + option.slice(1)} + +
    +
    +
  2. + ))} +
+ )} \ No newline at end of file diff --git a/app/[locale]/faculty-and-staff/page.tsx b/app/[locale]/faculty-and-staff/page.tsx index 740ed5e97..4ebc87cc4 100644 --- a/app/[locale]/faculty-and-staff/page.tsx +++ b/app/[locale]/faculty-and-staff/page.tsx @@ -3,7 +3,7 @@ export const revalidate = 300; import Image from 'next/image'; import Link from 'next/link'; -import { Suspense } from 'react'; +import { Suspense ,useMemo} from 'react'; import { FaPhone } from 'react-icons/fa6'; import { MdEmail } from 'react-icons/md'; @@ -23,7 +23,12 @@ import { cn } from '~/lib/utils'; import { db } from '~/server/db'; import { ScrollArea } from '~/components/ui/scroll-area'; -import { ClearFiltersButton, PreserveParamsLink } from './client-components'; +import { + ClearFiltersButton, + DepartmentsClient, + MobileFilters, + PreserveParamsLink, +} from './client-components'; export default async function FacultyAndStaff({ params: { locale }, @@ -38,6 +43,10 @@ export default async function FacultyAndStaff({ }) { const text = (await getTranslations(locale)).FacultyAndStaff; + const mobileDepartments = await db.query.departments.findMany({ + columns: { id: true, name: true, urlName: true }, + }); + return (
Filter By
- {/* Designation Filter Box */}
@@ -68,6 +76,7 @@ export default async function FacultyAndStaff({

Department

+ }> @@ -76,9 +85,9 @@ export default async function FacultyAndStaff({
- + - {/* Mobile Designation Filter */} - }> - - - - {/* Mobile Department Filter */} - }> - - +
+ +
    @@ -139,6 +145,16 @@ const Designations = ({ ? [designation] : []; + const sortedOptions = useMemo(() => { + return [...options].sort((a, b) => { + const aSelected = selectedDesignations.includes(a); + const bSelected = selectedDesignations.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + }, [selectedDesignations]); + // Define the updated designation value based on selection const getUpdatedDesignations = (option: string) => { return selectedDesignations.includes(option) @@ -158,7 +174,7 @@ const Designations = ({ /> - {options.map((option, index) => ( + {sortedOptions.map((option, index) => (
    ) : (
      - {options.map((option, index) => ( + {sortedOptions.map((option, index) => (
    1. { - return selectedDepartments.includes(urlName) - ? selectedDepartments.filter((d) => d !== urlName) - : [...selectedDepartments, urlName]; - }; + // // Define the updated department value based on selection + // const getUpdatedDepartments = (urlName: string) => { + // return selectedDepartments.includes(urlName) + // ? selectedDepartments.filter((d) => d !== urlName) + // : [...selectedDepartments, urlName]; + // }; - return select ? ( - - - {name} - -
    - ))} -
    - - ) : ( -
      - {departments.map(({ name, urlName }, index) => ( -
    1. - -
      -
      - -
      - {name} -
      -
      -
    2. - ))} -
    + // return select ? ( + // + // + // {name} + // + //
+ // ))} + // + // + // ) : ( + //
    + // {departments.map(({ name, urlName }, index) => ( + //
  1. + // + //
    + //
    + // + //
    + // {name} + //
    + //
    + //
  2. + // ))} + //
+ // ); + return ( + ); }; diff --git a/app/[locale]/faculty-and-staff/utils.tsx b/app/[locale]/faculty-and-staff/utils.tsx index f4f8509f0..6b709b176 100644 --- a/app/[locale]/faculty-and-staff/utils.tsx +++ b/app/[locale]/faculty-and-staff/utils.tsx @@ -46,8 +46,9 @@ import { students, } from '~/server/db'; + // Contains the content of full Faculty Profile -async function FacultyOrStaffComponent({ +export async function FacultyOrStaffComponent({ children, employeeId, id, @@ -433,7 +434,7 @@ const facultyTables = { outreachActivities: outreachActivities, } as const; -async function FacultySectionComponent({ +export async function FacultySectionComponent({ locale, facultySection, id, @@ -812,4 +813,3 @@ async function fetchSectionByFacultyId( )?.[section]; } -export { FacultyOrStaffComponent, FacultySectionComponent }; From a0a799099b44eb0b8cfa85081daab09a6aee36b3 Mon Sep 17 00:00:00 2001 From: Aryawart Kathpal <132134276+Aryawart-kathpal@users.noreply.github.com> Date: Mon, 12 Jan 2026 16:43:23 +0530 Subject: [PATCH 06/73] fix: Error fixes to Administration and Image Header (#431) --- app/[locale]/academics/curricula/page.tsx | 2 +- app/[locale]/academics/page.tsx | 4 ++-- .../clubs/[display_name]/event-section.tsx | 15 +++---------- .../clubs/[display_name]/page.tsx | 5 ++--- components/image-header.tsx | 22 ++++++++++--------- 5 files changed, 20 insertions(+), 28 deletions(-) diff --git a/app/[locale]/academics/curricula/page.tsx b/app/[locale]/academics/curricula/page.tsx index 8c010c20f..b6f017ada 100644 --- a/app/[locale]/academics/curricula/page.tsx +++ b/app/[locale]/academics/curricula/page.tsx @@ -91,7 +91,7 @@ const Courses = async ({ page }: { page: number }) => { offset: (page - 1) * 10, }); - console.log(courses); + // console.log(courses); return courses.map(({ code, coursesToMajors, title }) => { if (coursesToMajors.length === 0) { diff --git a/app/[locale]/academics/page.tsx b/app/[locale]/academics/page.tsx index e12c0281d..62bb77fc6 100644 --- a/app/[locale]/academics/page.tsx +++ b/app/[locale]/academics/page.tsx @@ -144,7 +144,7 @@ export default async function Academics({ />
(null); @@ -46,7 +38,7 @@ export default function EventsSection({

@@ -93,7 +85,6 @@ export default function EventsSection({

{selectedEvent.description}

- )} diff --git a/app/[locale]/student-activities/clubs/[display_name]/page.tsx b/app/[locale]/student-activities/clubs/[display_name]/page.tsx index 30e021147..820bd4ffb 100644 --- a/app/[locale]/student-activities/clubs/[display_name]/page.tsx +++ b/app/[locale]/student-activities/clubs/[display_name]/page.tsx @@ -347,12 +347,11 @@ export default async function Club({ <> -
+

{club?.aboutUs} diff --git a/components/image-header.tsx b/components/image-header.tsx index 72232b029..4aa6c0e13 100644 --- a/components/image-header.tsx +++ b/components/image-header.tsx @@ -10,14 +10,12 @@ export default function ImageHeader({ title, headings, src, - display_name, logoUrl, }: { className?: string; title?: string; headings?: { label: string; href: string }[]; src: string; - display_name?: string; logoUrl?: string; }) { return ( @@ -42,18 +40,17 @@ export default function ImageHeader({ backgroundImage: `url('${getS3Url()}/${src ? src : 'assets/landingpagebg-1.png'}')`, // FIXME: remove this hack once we have good images }} > - {title && ( + {title && !logoUrl && (

{title}

)} - {/* In case and image or logo is required on top of it */} - {display_name && logoUrl && ( -
+ {/* Title with logo - displayed side by side */} + {title && logoUrl && ( +
{display_name}

- {display_name.toUpperCase()} + {title.toUpperCase()}

)} + {/* Spacer div when no headings - pushes content below the absolute header */} + {!headings && ( +
+ )} + {headings && ( <>
    ); -} \ No newline at end of file +} From eb10858f0bd9594082dfc2be89d68e12bfa66fdc Mon Sep 17 00:00:00 2001 From: Yashika Choudhary <161009245+yashika1221@users.noreply.github.com> Date: Wed, 14 Jan 2026 14:25:23 +0530 Subject: [PATCH 07/73] Fixed Pagination Component --- app/[locale]/academics/curricula/page.tsx | 2 +- .../administration/(committees)/committee.tsx | 4 +- app/[locale]/research/page.tsx | 137 +++++++++--------- .../patents-and-technologies/page.tsx | 8 +- components/pagination/pagination.tsx | 49 ++++--- components/ui/generic-table.tsx | 3 +- 6 files changed, 100 insertions(+), 103 deletions(-) diff --git a/app/[locale]/academics/curricula/page.tsx b/app/[locale]/academics/curricula/page.tsx index b6f017ada..f6aa1edba 100644 --- a/app/[locale]/academics/curricula/page.tsx +++ b/app/[locale]/academics/curricula/page.tsx @@ -67,7 +67,7 @@ export default async function Curricula({
diff --git a/app/[locale]/institute/administration/(committees)/committee.tsx b/app/[locale]/institute/administration/(committees)/committee.tsx index 62f846a48..0297199c4 100644 --- a/app/[locale]/institute/administration/(committees)/committee.tsx +++ b/app/[locale]/institute/administration/(committees)/committee.tsx @@ -83,10 +83,10 @@ export default async function Committee({ ); diff --git a/app/[locale]/research/page.tsx b/app/[locale]/research/page.tsx index a3af21e1e..a1fb498fd 100644 --- a/app/[locale]/research/page.tsx +++ b/app/[locale]/research/page.tsx @@ -36,18 +36,28 @@ type CopyrightsTable = typeof copyrights.$inferSelect; type DesignsTable = typeof designs.$inferSelect; type ResearchAndConsultancyTable = typeof researchAndConsultancy.$inferSelect; type Moustable = typeof mous.$inferSelect; -// type SponsoredProjectsTable = typeof sponsoredResearchProjects.$inferSelect; -// type SponsoredProjectsFacultiesTable = typeof sponsoredResearchProjectsFaculties.$inferSelect; - export default async function PatentsAndTechnology({ params: { locale }, searchParams, }: { params: { locale: string }; - searchParams?: { page?: string }; + searchParams?: { + researchPage?: string; + patentsPage?: string; + copyrightsPage?: string; + designsPage?: string; + memorandumPage?: string; + projectsPage?: string; + }; }) { - const currentPage = Number(searchParams?.page ?? 1); + // Individual page states for each table + const researchPage = Number(searchParams?.researchPage ?? 1); + const patentsPage = Number(searchParams?.patentsPage ?? 1); + const copyrightsPage = Number(searchParams?.copyrightsPage ?? 1); + const designsPage = Number(searchParams?.designsPage ?? 1); + const memorandumPage = Number(searchParams?.memorandumPage ?? 1); + const projectsPage = Number(searchParams?.projectsPage ?? 1); const text = (await getTranslations(locale)).Research; @@ -95,7 +105,7 @@ export default async function PatentsAndTechnology({ }, }, }, - orderBy: (rc) => sql`SUBSTRING(${rc.year}, 1, 4)::integer DESC`, // sorting by year to latest + orderBy: (rc) => sql`SUBSTRING(${rc.year}, 1, 4)::integer DESC`, } ); const mous = await db.query.mous.findMany(); @@ -107,19 +117,19 @@ export default async function PatentsAndTechnology({ }; }); - const projects= await db.query.sponsoredResearchProjects.findMany({ + const projects = await db.query.sponsoredResearchProjects.findMany({ with: { faculties: true, - department: true, // Make sure to also include department relation + department: true, }, }); - - + // Transform projects data into the format needed for the table const staticProjects = projects.map((project) => { - const facultyNames = project.faculties && project.faculties.length > 0 - ? project.faculties.map((faculty) => faculty.facultyName).join(', ') - : 'N/A'; + const facultyNames = + project.faculties && project.faculties.length > 0 + ? project.faculties.map((faculty) => faculty.facultyName).join(', ') + : 'N/A'; return { year: project.year, @@ -133,34 +143,8 @@ export default async function PatentsAndTechnology({ status: project.status ?? 'N/A', }; }); - const base = getS3Url(); - // Get the total count for pagination - const getResearchCount = async () => { - const count = researchAndConsultancy.length; // Replace with your actual DB call - return [{ count }]; - }; - const getProjectCount = async () => { - const count = staticProjects.length; // Replace with your actual DB call - return [{ count }]; - }; - const getPatentCount = async () => { - const count = patents.length; // Replace with your actual DB call - return [{ count }]; - }; - const getMemorandumCount = async () => { - const count = formattedMemorandum.length; // Replace with your actual DB call - return [{ count }]; - }; - - const getCopyrightsCount = async () => { - const count = copyrights.length; // Replace with your actual DB call - return [{ count }]; - }; - const getDesignsCount = async () => { - const count = designs.length; - return [{ count }]; - }; + const base = getS3Url(); return ( <> @@ -222,7 +206,7 @@ export default async function PatentsAndTechnology({ > @@ -232,11 +216,13 @@ export default async function PatentsAndTechnology({
+ {/* PATENTS AND TECHNOLOGIES */}
@@ -284,11 +270,13 @@ export default async function PatentsAndTechnology({
+ {/* COPYRIGHTS AND DESIGNS */}

{text.sections.copyright.design}

+ {/* DESIGNS TABLE */}
@@ -362,7 +352,7 @@ export default async function PatentsAndTechnology({ }> @@ -371,12 +361,14 @@ export default async function PatentsAndTechnology({
+ {/* MOU */}
@@ -422,11 +414,13 @@ export default async function PatentsAndTechnology({
+ {/* SPONSORED PROJECTS */}
( - {headerText} + + {headerText} + ))} @@ -461,7 +460,7 @@ export default async function PatentsAndTechnology({ - + @@ -469,7 +468,7 @@ export default async function PatentsAndTechnology({ > @@ -479,11 +478,13 @@ export default async function PatentsAndTechnology({
+ {/* IMPORTANT RESOURCES */}
    - {' '} {archiveLinks.map((item, index) => (
  • {visibleData.map((item, index) => { const cellData = [ - startIndex + index + 1, // S. No. + startIndex + index + 1, item.grantYear, item.copyrightNo, item.title, @@ -623,7 +623,7 @@ const DesignTable = ({ <> {visibleData.map((item, index) => { const cellData = [ - startIndex + index + 1, // S. No. + startIndex + index + 1, item.dateOfRegistration, item.designNumber, item.title, @@ -708,11 +708,7 @@ const MemorandumTable = ({ return ( <> {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, // serial no. - item.organization, - item.date, - ]; + const cellData = [startIndex + index + 1, item.organization, item.date]; return ( ); }; + const ProjectsTable = ({ tableData, currentPage, @@ -754,7 +751,7 @@ const ProjectsTable = ({ <> {visibleData.map((item, index) => { const cellData = [ - startIndex + index + 1, // S. No. + startIndex + index + 1, item.year, item.department, item.facultyName, diff --git a/app/[locale]/research/patents-and-technologies/page.tsx b/app/[locale]/research/patents-and-technologies/page.tsx index 19ded8595..49f3587de 100644 --- a/app/[locale]/research/patents-and-technologies/page.tsx +++ b/app/[locale]/research/patents-and-technologies/page.tsx @@ -98,11 +98,7 @@ export default async function PatentsAndTechnology({ }, ]; - // Get the total count for pagination - const getPatentCount = async () => { - const count = staticPatents.length; // Replace with your actual DB call - return [{ count }]; - }; + const totalCount = staticPatents.length; return ( <> @@ -149,7 +145,7 @@ export default async function PatentsAndTechnology({
diff --git a/components/pagination/pagination.tsx b/components/pagination/pagination.tsx index fac5f89b2..8c7c0da49 100644 --- a/components/pagination/pagination.tsx +++ b/components/pagination/pagination.tsx @@ -1,3 +1,7 @@ +'use client'; + +import { useSearchParams } from 'next/navigation'; + import { cn } from '~/lib/utils'; import { @@ -10,17 +14,25 @@ import { PaginationPrevious, } from '.'; -export const PaginationWithLogic = async ({ +export const PaginationWithLogic = ({ className, currentPage, - query, + totalCount, + pageParamName = 'page', ...props }: React.ComponentProps<'nav'> & { currentPage: number; - query: Promise<{ count: number }[]>; + totalCount: number; + pageParamName?: string; }) => { - const rows = await query; - const noOfPages = Math.ceil(Number(rows[0].count) / 10); + const searchParams = useSearchParams(); + const noOfPages = Math.ceil(totalCount / 10); + + const createHref = (pageNumber: number) => { + const params = new URLSearchParams(searchParams.toString()); + params.set(pageParamName, pageNumber.toString()); + return `?${params.toString()}`; + }; return ( @@ -28,34 +40,25 @@ export const PaginationWithLogic = async ({ - + 1 {noOfPages > 1 && (currentPage < 4 || noOfPages < 6) && ( - + 2 )} {noOfPages > 2 && (currentPage < 4 || noOfPages < 6) && ( - + 3 @@ -66,7 +69,7 @@ export const PaginationWithLogic = async ({ <> {currentPage} @@ -79,7 +82,7 @@ export const PaginationWithLogic = async ({ {noOfPages > 5 && currentPage > noOfPages - 3 && ( {noOfPages - 2} @@ -89,7 +92,7 @@ export const PaginationWithLogic = async ({ {noOfPages > 4 && (currentPage > noOfPages - 3 || noOfPages < 6) && ( {noOfPages - 1} @@ -99,7 +102,7 @@ export const PaginationWithLogic = async ({ {noOfPages > 3 && ( {noOfPages} @@ -110,7 +113,7 @@ export const PaginationWithLogic = async ({ = noOfPages} - href={{ query: { page: currentPage + 1 } }} + href={createHref(currentPage + 1)} /> diff --git a/components/ui/generic-table.tsx b/components/ui/generic-table.tsx index 5cb62d42e..4efdc9d18 100644 --- a/components/ui/generic-table.tsx +++ b/components/ui/generic-table.tsx @@ -35,6 +35,7 @@ export function GenericTable>({ }: GenericTableProps) { const startIndex = (currentPage - 1) * itemsPerPage; const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); + const totalCount = tableData.length; return (
@@ -79,7 +80,7 @@ export function GenericTable>({
- +
); From f66622b23cce4660b083572ff8915baf3575d6c4 Mon Sep 17 00:00:00 2001 From: Abhay Agarwal <161469723+Abhay145@users.noreply.github.com> Date: Thu, 15 Jan 2026 00:05:59 +0530 Subject: [PATCH 08/73] Gallery Carousel Fix --- app/[locale]/institute/cells/obcpwd/page.tsx | 77 ++++++++-------- app/[locale]/institute/cells/scst/page.tsx | 92 ++++++++++--------- .../clubs/[display_name]/event-section.tsx | 14 +-- app/[locale]/student-activities/page.tsx | 24 +++-- components/carousels/gallery.tsx | 19 ++-- 5 files changed, 117 insertions(+), 109 deletions(-) diff --git a/app/[locale]/institute/cells/obcpwd/page.tsx b/app/[locale]/institute/cells/obcpwd/page.tsx index 036f0eda5..d3679a9a1 100644 --- a/app/[locale]/institute/cells/obcpwd/page.tsx +++ b/app/[locale]/institute/cells/obcpwd/page.tsx @@ -15,9 +15,7 @@ export default async function OBCPWD({ }) { const text = await getTranslations(locale); - const facultyIncharge = [ - {...text.Institute.cells.obcpwd.liaisonOfficer}, - ]; + const facultyIncharge = [{ ...text.Institute.cells.obcpwd.liaisonOfficer }]; const cellFunctions = text.Institute.cells.obcpwd.cellFunctions; return ( @@ -29,8 +27,8 @@ export default async function OBCPWD({ backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0.5), rgba(0,0,0,0.3)), url('${getS3Url()}/training-and-placement/header.jpg')`, }} > -
-

+
+

{text.Institute.cells.obcpwd.title}

@@ -38,7 +36,7 @@ export default async function OBCPWD({
{/* description */} -
+
{text.Institute.cells.obcpwd.description.map((paragraph, index) => (

{paragraph} @@ -54,18 +52,18 @@ export default async function OBCPWD({ className="mt-12" />

-
    - {cellFunctions.map((functionItem, index) => ( -
  • - - {functionItem} -
  • - ))} +
      + {cellFunctions.map((functionItem, index) => ( +
    • + + {functionItem} +
    • + ))}
-
+
{text.Institute.cells.obcpwd.complaint} -
+

{/* LIAISON OFFICER */}
@@ -82,56 +80,62 @@ export default async function OBCPWD({ className="flex w-[90%] max-w-3xl rounded-lg border border-primary-500 bg-neutral-50 p-1 " > {/* Image - smaller on mobile */} -
+
{faculty.name}
- + {/* Content section - adjusted for mobile row layout */} -
+
{/* Name in red - reduced margin bottom */} -

+

{faculty.name}

- + {/* Title and position - reduced spacing */} -
{/* Reduced margin from mb-1/mb-2/mb-4 */} -

{/* Added leading-tight */} +

+ {' '} + {/* Reduced margin from mb-1/mb-2/mb-4 */} +

+ {' '} + {/* Added leading-tight */} {faculty.title}

- {!faculty.title.includes("Head") && ( -

{/* Added leading-tight and mt-0 */} + {!faculty.title.includes('Head') && ( +

+ {' '} + {/* Added leading-tight and mt-0 */} (Head of the Department)

)}
- + {/* Contact info */} -
+
{/* Email with icon */} - - + + - {faculty.email} - + {/* Phone with icon */} - - + + - + {faculty.phone} @@ -141,9 +145,8 @@ export default async function OBCPWD({ ))}
- + {/* TODO: MAKE IT EXACTLY LIKE THE DESIGN , ADD RELEVENT BACKGROUND */} - ); diff --git a/app/[locale]/institute/cells/scst/page.tsx b/app/[locale]/institute/cells/scst/page.tsx index eb65a4b1a..ec92196fa 100644 --- a/app/[locale]/institute/cells/scst/page.tsx +++ b/app/[locale]/institute/cells/scst/page.tsx @@ -15,9 +15,7 @@ export default async function SCST({ }) { const text = await getTranslations(locale); - const facultyIncharge = [ - {...text.Institute.cells.scst.liaisonOfficer}, - ]; + const facultyIncharge = [{ ...text.Institute.cells.scst.liaisonOfficer }]; const cellFunctions = text.Institute.cells.scst.cellFunctions; const importantLinks = text.Institute.cells.scst.importantLinks; @@ -30,8 +28,8 @@ export default async function SCST({ backgroundImage: `linear-gradient(to bottom, rgba(0,0,0,0.5), rgba(0,0,0,0.3)), url('${getS3Url()}/training-and-placement/header.jpg')`, }} > -
-

+
+

{text.Institute.cells.scst.title}

@@ -39,7 +37,7 @@ export default async function SCST({
{/* description */} -
+
{text.Institute.cells.scst.description.map((paragraph, index) => (

{paragraph} @@ -55,18 +53,18 @@ export default async function SCST({ className="mt-12" />

-
    - {cellFunctions.map((functionItem, index) => ( -
  • - - {functionItem} -
  • - ))} +
      + {cellFunctions.map((functionItem, index) => ( +
    • + + {functionItem} +
    • + ))}
-
+
{text.Institute.cells.scst.complaint} -
+

{/* LIAISON OFFICER */}
@@ -83,56 +81,62 @@ export default async function SCST({ className="flex w-[90%] max-w-3xl rounded-lg border border-primary-500 bg-neutral-50 p-1 " > {/* Image - smaller on mobile */} -
+
{faculty.name}
- + {/* Content section - adjusted for mobile row layout */} -
+
{/* Name in red - reduced margin bottom */} -

+

{faculty.name}

- + {/* Title and position - reduced spacing */} -
{/* Reduced margin from mb-1/mb-2/mb-4 */} -

{/* Added leading-tight */} +

+ {' '} + {/* Reduced margin from mb-1/mb-2/mb-4 */} +

+ {' '} + {/* Added leading-tight */} {faculty.title}

- {!faculty.title.includes("Head") && ( -

{/* Added leading-tight and mt-0 */} + {!faculty.title.includes('Head') && ( +

+ {' '} + {/* Added leading-tight and mt-0 */} (Head of the Department)

)}
- + {/* Contact info */} -
+
{/* Email with icon */} - - + + - {faculty.email} - + {/* Phone with icon */} - - + + - + {faculty.phone} @@ -151,12 +155,12 @@ export default async function SCST({ text={text.Institute.cells.scst.importantLinksHeading} className="mt-12" /> - +
- - ); diff --git a/app/[locale]/student-activities/clubs/[display_name]/event-section.tsx b/app/[locale]/student-activities/clubs/[display_name]/event-section.tsx index 6e6babaf5..78cff6bce 100644 --- a/app/[locale]/student-activities/clubs/[display_name]/event-section.tsx +++ b/app/[locale]/student-activities/clubs/[display_name]/event-section.tsx @@ -56,19 +56,19 @@ export default function EventsSection({ onOpenChange={(open) => !open && setSelectedEvent(null)} > {selectedEvent && ( <> -

+

{selectedEvent.title}

{selectedEvent.image.map((img, index) => ( -

+

{selectedEvent.description}

diff --git a/app/[locale]/student-activities/page.tsx b/app/[locale]/student-activities/page.tsx index 03610ff61..538321dda 100644 --- a/app/[locale]/student-activities/page.tsx +++ b/app/[locale]/student-activities/page.tsx @@ -82,9 +82,10 @@ const ClubsCarousel = async ({ locale }: { locale: string }) => { href={`/${locale}/student-activities/clubs/${urlName}`} key={index} > - + { {alias {alias ?? name} - + {alias diff --git a/components/carousels/gallery.tsx b/components/carousels/gallery.tsx index 24371f274..ec3a2d47d 100644 --- a/components/carousels/gallery.tsx +++ b/components/carousels/gallery.tsx @@ -12,7 +12,7 @@ const GalleryCarousel = ({ carouselProps, children, className, - itemClassName = 'sm:basis-1/2 lg:basis-1/3 xl:basis-1/4 2xl:basis-1/5', + itemClassName = 'shrink-0 basis-full sm:basis-1/2 lg:basis-1/3 xl:basis-1/4 2xl:basis-1/5', }: { carouselProps?: CarouselProps; children: React.ReactNode[]; @@ -21,26 +21,19 @@ const GalleryCarousel = ({ }) => { return (
- - + + {children.map((child, index) => ( {child} ))} - - + +
); }; -export { GalleryCarousel }; \ No newline at end of file +export { GalleryCarousel }; From 24454ab68b9c0d5751cf45726cf53ed3b3d6143b Mon Sep 17 00:00:00 2001 From: Debatreya Date: Thu, 15 Jan 2026 00:34:54 +0530 Subject: [PATCH 09/73] Fix: Fixed broken link in the search cards --- app/[locale]/academics/curricula/page.tsx | 9 +- app/[locale]/events.tsx | 2 +- .../faculty-and-staff/client-components.tsx | 211 +++++++++--------- app/[locale]/faculty-and-staff/page.tsx | 28 ++- app/[locale]/faculty-and-staff/utils.tsx | 2 - .../administration/(committees)/committee.tsx | 12 +- app/[locale]/research/page.tsx | 5 +- .../student-activities/clubs/page.tsx | 2 +- components/message-card.tsx | 2 +- components/ui/generic-table.tsx | 5 +- i18n/hi.ts | 20 +- server/db/schema/index.ts | 2 +- ...ored-research-projects-faculties.schema.ts | 24 +- .../sponsored-research-projects.schema.ts | 32 +-- 14 files changed, 189 insertions(+), 167 deletions(-) diff --git a/app/[locale]/academics/curricula/page.tsx b/app/[locale]/academics/curricula/page.tsx index f6aa1edba..76a3918ac 100644 --- a/app/[locale]/academics/curricula/page.tsx +++ b/app/[locale]/academics/curricula/page.tsx @@ -67,7 +67,10 @@ export default async function Curricula({ @@ -123,6 +126,6 @@ const Courses = async ({ page }: { page: number }) => { ) - ) -}); + ); + }); }; diff --git a/app/[locale]/events.tsx b/app/[locale]/events.tsx index 3debc3eff..44ec81c9a 100644 --- a/app/[locale]/events.tsx +++ b/app/[locale]/events.tsx @@ -176,4 +176,4 @@ export default async function Events({ ); -} \ No newline at end of file +} diff --git a/app/[locale]/faculty-and-staff/client-components.tsx b/app/[locale]/faculty-and-staff/client-components.tsx index 8ae886e20..e7f4a3296 100644 --- a/app/[locale]/faculty-and-staff/client-components.tsx +++ b/app/[locale]/faculty-and-staff/client-components.tsx @@ -52,7 +52,6 @@ export function MobileFilters({ }; }, [open]); - return (
{/* Filter button */} @@ -73,87 +72,93 @@ export function MobileFilters({
setOpen(false)} > -
- {open && createPortal( - , - document.body - )} + , + document.body + )}
); } @@ -236,7 +241,7 @@ export function DepartmentsClient({ Array.isArray(department) ? department : department ? [department] : [], [department] ); - const sortedDepartments = useMemo(() => { + const sortedDepartments = useMemo(() => { return [...departments].sort((a, b) => { const aSelected = selectedDepartments.includes(a.urlName); const bSelected = selectedDepartments.includes(b.urlName); @@ -288,7 +293,9 @@ export function DepartmentsClient({ } // Desktop/list mode: show limited items and "View more" button that toggles full list. - const visible = showAll ? sortedDepartments : sortedDepartments.slice(0, optionsToShow); + const visible = showAll + ? sortedDepartments + : sortedDepartments.slice(0, optionsToShow); return (
@@ -337,9 +344,7 @@ export function DepartmentsClient({ className="text-primary-700 underline" aria-expanded={showAll} > - {showAll - ? 'View less' - : 'View more'} + {showAll ? 'View less' : 'View more'}
)} @@ -365,7 +370,6 @@ function DesignationsClient() { }); }, [selected]); - const getUpdatedDesignations = (option: string) => selected.includes(option) ? selected.filter((d) => d !== option) @@ -373,34 +377,35 @@ function DesignationsClient() { return (
    - {sortedOptions.map((option, index) => ( -
  1. - -
    -
    - -
    - - {option.charAt(0).toUpperCase() + option.slice(1)} - -
    -
    -
  2. - ))} -
- )} \ No newline at end of file + {sortedOptions.map((option, index) => ( +
  • + +
    +
    + +
    + + {option.charAt(0).toUpperCase() + option.slice(1)} + +
    +
    +
  • + ))} + + ); +} diff --git a/app/[locale]/faculty-and-staff/page.tsx b/app/[locale]/faculty-and-staff/page.tsx index 4ebc87cc4..84e3aac28 100644 --- a/app/[locale]/faculty-and-staff/page.tsx +++ b/app/[locale]/faculty-and-staff/page.tsx @@ -3,7 +3,7 @@ export const revalidate = 300; import Image from 'next/image'; import Link from 'next/link'; -import { Suspense ,useMemo} from 'react'; +import { Suspense, useMemo } from 'react'; import { FaPhone } from 'react-icons/fa6'; import { MdEmail } from 'react-icons/md'; @@ -145,15 +145,15 @@ const Designations = ({ ? [designation] : []; - const sortedOptions = useMemo(() => { - return [...options].sort((a, b) => { - const aSelected = selectedDesignations.includes(a); - const bSelected = selectedDesignations.includes(b); - if (aSelected && !bSelected) return -1; - if (!aSelected && bSelected) return 1; - return 0; - }); - }, [selectedDesignations]); + const sortedOptions = useMemo(() => { + return [...options].sort((a, b) => { + const aSelected = selectedDesignations.includes(a); + const bSelected = selectedDesignations.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + }, [selectedDesignations]); // Define the updated designation value based on selection const getUpdatedDesignations = (option: string) => { @@ -468,7 +468,13 @@ const FacultyList = async ({ {key}
    ); diff --git a/app/[locale]/research/page.tsx b/app/[locale]/research/page.tsx index a1fb498fd..55c714579 100644 --- a/app/[locale]/research/page.tsx +++ b/app/[locale]/research/page.tsx @@ -447,10 +447,7 @@ export default async function PatentsAndTechnology({ text.projects.sanctionedDate, text.projects.status, ].map((headerText, index) => ( - + {headerText} ))} diff --git a/app/[locale]/student-activities/clubs/page.tsx b/app/[locale]/student-activities/clubs/page.tsx index 3a06ea81a..91e3559b1 100644 --- a/app/[locale]/student-activities/clubs/page.tsx +++ b/app/[locale]/student-activities/clubs/page.tsx @@ -86,4 +86,4 @@ export default async function Clubs({ ); -} \ No newline at end of file +} diff --git a/components/message-card.tsx b/components/message-card.tsx index d18604337..84ceeb9fd 100644 --- a/components/message-card.tsx +++ b/components/message-card.tsx @@ -93,4 +93,4 @@ export default function MessageCard({ )} ); -} \ No newline at end of file +} diff --git a/components/ui/generic-table.tsx b/components/ui/generic-table.tsx index 4efdc9d18..1e45b0a59 100644 --- a/components/ui/generic-table.tsx +++ b/components/ui/generic-table.tsx @@ -80,7 +80,10 @@ export function GenericTable>({
    - +
    ); diff --git a/i18n/hi.ts b/i18n/hi.ts index a1f8ffcc0..5cef46703 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -1655,16 +1655,16 @@ const text: Translations = { signingDate: 'हस्ताक्षर तिथि', }, projects: { - number: 'क्रम संख्या', - year: 'वर्ष', - department: 'विभाग', - facultyName: 'संकाय का नाम', - title: 'परियोजना का शीर्षक', - agency: 'एजेंसी', - amount: 'राशि (रु.) लाख में', - sanctionedFileOrderNo: 'स्वीकृत फाइल/आदेश संख्या', - sanctionedDate: 'स्वीकृत तिथि', - status: 'स्थिति', + number: 'क्रम संख्या', + year: 'वर्ष', + department: 'विभाग', + facultyName: 'संकाय का नाम', + title: 'परियोजना का शीर्षक', + agency: 'एजेंसी', + amount: 'राशि (रु.) लाख में', + sanctionedFileOrderNo: 'स्वीकृत फाइल/आदेश संख्या', + sanctionedDate: 'स्वीकृत तिथि', + status: 'स्थिति', }, archive: { title: 'संग्रह', diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index 1fbe1eb5d..07a5786e9 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -34,4 +34,4 @@ export * from './student-academic-details.schema'; export * from './students.schema'; export * from './sponsored-research-projects.schema'; export * from './sponsored-research-projects-faculties.schema'; -export * from './memorandum.schema'; +export * from './memorandum.schema'; diff --git a/server/db/schema/sponsored-research-projects-faculties.schema.ts b/server/db/schema/sponsored-research-projects-faculties.schema.ts index c216468bd..8fe7dbbcb 100644 --- a/server/db/schema/sponsored-research-projects-faculties.schema.ts +++ b/server/db/schema/sponsored-research-projects-faculties.schema.ts @@ -1,11 +1,17 @@ -import {pgTable} from "drizzle-orm/pg-core"; -import { sponsoredResearchProjects } from "./sponsored-research-projects.schema"; -import { relations } from "drizzle-orm"; -export const sponsoredResearchProjectsFaculties = pgTable('sponsored_research_projects_faculties', (t) => ({ - id: t.serial('id').primaryKey(), - sponsoredResearchProjectId: t.integer('sponsored_research_project_id').references(() => sponsoredResearchProjects.id).notNull(), - facultyName: t.varchar('faculty_name').notNull(), -})); +import { pgTable } from 'drizzle-orm/pg-core'; +import { sponsoredResearchProjects } from './sponsored-research-projects.schema'; +import { relations } from 'drizzle-orm'; +export const sponsoredResearchProjectsFaculties = pgTable( + 'sponsored_research_projects_faculties', + (t) => ({ + id: t.serial('id').primaryKey(), + sponsoredResearchProjectId: t + .integer('sponsored_research_project_id') + .references(() => sponsoredResearchProjects.id) + .notNull(), + facultyName: t.varchar('faculty_name').notNull(), + }) +); export const sponsoredResearchProjectsFacultiesRelations = relations( sponsoredResearchProjectsFaculties, @@ -15,4 +21,4 @@ export const sponsoredResearchProjectsFacultiesRelations = relations( references: [sponsoredResearchProjects.id], }), }) -); \ No newline at end of file +); diff --git a/server/db/schema/sponsored-research-projects.schema.ts b/server/db/schema/sponsored-research-projects.schema.ts index 83560d765..cff5e9e36 100644 --- a/server/db/schema/sponsored-research-projects.schema.ts +++ b/server/db/schema/sponsored-research-projects.schema.ts @@ -1,28 +1,28 @@ -import { sql } from "drizzle-orm"; -import { pgTable } from "drizzle-orm/pg-core"; -import { relations } from "drizzle-orm"; -import { departments } from "./departments.schema"; -import { sponsoredResearchProjectsFaculties } from "./sponsored-research-projects-faculties.schema"; +import { sql } from 'drizzle-orm'; +import { pgTable } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; +import { departments } from './departments.schema'; +import { sponsoredResearchProjectsFaculties } from './sponsored-research-projects-faculties.schema'; export const sponsoredResearchProjects = pgTable( - "sponsored_research_projects", + 'sponsored_research_projects', (t) => ({ - id: t.serial("id").primaryKey(), - year: t.varchar("year").notNull(), + id: t.serial('id').primaryKey(), + year: t.varchar('year').notNull(), departmentId: t - .smallserial("department_id") + .smallserial('department_id') .references(() => departments.id) .notNull(), - titleOfProject: t.varchar("title_of_project").notNull(), - agency: t.varchar("agency").notNull(), + titleOfProject: t.varchar('title_of_project').notNull(), + agency: t.varchar('agency').notNull(), amountInLakh: t - .numeric("amount_in_lakh", { precision: 10, scale: 2 }) + .numeric('amount_in_lakh', { precision: 10, scale: 2 }) .notNull(), - sanctionedFileOrderNO: t.varchar("sanctioned_file_order_no"), - sanctionedDate: t.date("sanctioned_date"), + sanctionedFileOrderNO: t.varchar('sanctioned_file_order_no'), + sanctionedDate: t.date('sanctioned_date'), status: t - .varchar("status", { enum: ["ongoing", "completed"] }) - .default("ongoing") + .varchar('status', { enum: ['ongoing', 'completed'] }) + .default('ongoing') .notNull(), }), (t) => ({ From 47317b6a5fa3f4b7f2a56827764efd9a3d5e755b Mon Sep 17 00:00:00 2001 From: Debatreya Date: Thu, 15 Jan 2026 00:55:52 +0530 Subject: [PATCH 10/73] fix: External Scopus link --- lib/schemas/faculty-profile.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/schemas/faculty-profile.ts b/lib/schemas/faculty-profile.ts index b7e35c23e..723a9bcf4 100644 --- a/lib/schemas/faculty-profile.ts +++ b/lib/schemas/faculty-profile.ts @@ -127,7 +127,7 @@ export const facultyPersonalDetailsSchema = z.object({ scopusId: z .string() .regex( - /^(https?:\/\/)?(www.)?scopus.com\/authid\/detail.uri\?authorId=\d+-\d+$/, + /^(https?:\/\/)?(www.)?scopus.com\/authid\/detail.uri\?authorId=\d+-?\d+$/, 'Invalid Scopus URL format' ) .optional(), From 7f6088474f538725973b8ca0c1067390a53d96eb Mon Sep 17 00:00:00 2001 From: Debatreya Das <116421305+Debatreya@users.noreply.github.com> Date: Thu, 15 Jan 2026 02:45:16 +0530 Subject: [PATCH 11/73] Feat/faculty (#460) Image Uploading This pull request introduces a comprehensive faculty photo upload and display system, enabling users to upload profile photos for faculty and staff, and ensuring robust display of these images throughout the application. The changes include a new `FacultyPhotoUpload` component for uploading images, a `FacultyImage` component that intelligently loads images with fallback logic, and integration of these components across profile editing and listing pages. Additionally, minor UI and formatting improvements are included. **Faculty and Staff Photo Upload and Display** * Added a new `FacultyPhotoUpload` component that allows users to upload faculty/staff profile photos, including file type and size validation, image preview, S3 upload logic, and user feedback via toasts. ([app/[locale]/@modals/(.)profile/edit/faculty-photo-upload.tsxR1-R231](diffhunk://#diff-7866be717a5ec66dbf321ab271194ff475030422db1cdcfc38b03472f9189f72R1-R231)) * Introduced a `FacultyImage` component that attempts to load the faculty/staff image from S3 in multiple formats with graceful fallback to a default image, and replaced direct `` usage with this component in all relevant places: faculty list, staff list, and faculty profile. ([app/[locale]/faculty-and-staff/faculty-image.tsxR1-R73](diffhunk://#diff-f3054df127f38ef9044a3b238e2c892fffc15bbda73792eaedfe36286ea35c2fR1-R73), [app/[locale]/faculty-and-staff/page.tsxL407-L412](diffhunk://#diff-73d9b8e5183a03609e638663514db195677d4e964b2ba2898d7ad4aa250d81d6L407-L412), [app/[locale]/faculty-and-staff/page.tsxL581-L586](diffhunk://#diff-73d9b8e5183a03609e638663514db195677d4e964b2ba2898d7ad4aa250d81d6L581-L586), [app/[locale]/faculty-and-staff/utils.tsxL241-R248](diffhunk://#diff-3e362b10e143377150343a08121115b31eeefe1081b2d3cdc3bb5551eaec6b4dL241-R248)) * Integrated the photo upload section into the faculty profile edit modal, including fetching and passing the necessary props (`facultyId`, `employeeId`, `name`) to the upload component. ([app/[locale]/@modals/(.)profile/edit/page.tsxR25](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805R25), [app/[locale]/@modals/(.)profile/edit/page.tsxR55-R56](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805R55-R56), [app/[locale]/@modals/(.)profile/edit/page.tsxR67](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805R67), [app/[locale]/@modals/(.)profile/edit/page.tsxR79-R81](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805R79-R81), [app/[locale]/@modals/(.)profile/edit/page.tsxL98-R117](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805L98-R117)) **Faculty/Staff List and Profile Improvements** * Updated faculty and staff listing pages to use the new `FacultyImage` for consistent image handling, and improved external link handling for profile links to ensure proper URL formatting. ([app/[locale]/faculty-and-staff/page.tsxL407-L412](diffhunk://#diff-73d9b8e5183a03609e638663514db195677d4e964b2ba2898d7ad4aa250d81d6L407-L412), [app/[locale]/faculty-and-staff/page.tsxL471-R479](diffhunk://#diff-73d9b8e5183a03609e638663514db195677d4e964b2ba2898d7ad4aa250d81d6L471-R479), [app/[locale]/faculty-and-staff/page.tsxL581-L586](diffhunk://#diff-73d9b8e5183a03609e638663514db195677d4e964b2ba2898d7ad4aa250d81d6L581-L586)) * Updated the faculty/staff profile utility to use `FacultyImage` for image rendering. ([app/[locale]/faculty-and-staff/utils.tsxR49](diffhunk://#diff-3e362b10e143377150343a08121115b31eeefe1081b2d3cdc3bb5551eaec6b4dR49), [app/[locale]/faculty-and-staff/utils.tsxL241-R248](diffhunk://#diff-3e362b10e143377150343a08121115b31eeefe1081b2d3cdc3bb5551eaec6b4dL241-R248)) **UI and Code Formatting Enhancements** * Improved section headers and layout in the profile edit modal for clarity. ([app/[locale]/@modals/(.)profile/edit/page.tsxL98-R117](diffhunk://#diff-e73212c94224fb2541821a5e5cdf5880f9ecace5d712ec5cd8dff6e590ded805L98-R117)) * Minor code formatting and readability improvements in client components and curricula page. ([app/[locale]/academics/curricula/page.tsxL70-R73](diffhunk://#diff-f28e90cdfc2124ecddf73b3a9d4522b6270d42112fa9b8a81c67656ed7722c86L70-R73), [app/[locale]/academics/curricula/page.tsxL126-R129](diffhunk://#diff-f28e90cdfc2124ecddf73b3a9d4522b6270d42112fa9b8a81c67656ed7722c86L126-R129), [app/[locale]/faculty-and-staff/client-components.tsxL55](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L55), [app/[locale]/faculty-and-staff/client-components.tsxL76-R85](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L76-R85), [app/[locale]/faculty-and-staff/client-components.tsxL115-R118](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L115-R118), [app/[locale]/faculty-and-staff/client-components.tsxL126-R131](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L126-R131), [app/[locale]/faculty-and-staff/client-components.tsxL291-R298](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L291-R298), [app/[locale]/faculty-and-staff/client-components.tsxL340-R347](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L340-R347), [app/[locale]/faculty-and-staff/client-components.tsxL368](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L368), [app/[locale]/faculty-and-staff/client-components.tsxL406-R411](diffhunk://#diff-f3d956fa19c730d337793ff6235013b3be6594251230f0777f7bcc95cfb03726L406-R411)) --- .../(.)profile/edit/faculty-photo-upload.tsx | 231 ++++++++++++++++++ app/[locale]/@modals/(.)profile/edit/page.tsx | 21 +- .../faculty-and-staff/faculty-image.tsx | 73 ++++++ app/[locale]/faculty-and-staff/page.tsx | 19 +- app/[locale]/faculty-and-staff/utils.tsx | 27 +- server/actions/media-upload.ts | 69 ++++++ 6 files changed, 416 insertions(+), 24 deletions(-) create mode 100644 app/[locale]/@modals/(.)profile/edit/faculty-photo-upload.tsx create mode 100644 app/[locale]/faculty-and-staff/faculty-image.tsx create mode 100644 server/actions/media-upload.ts diff --git a/app/[locale]/@modals/(.)profile/edit/faculty-photo-upload.tsx b/app/[locale]/@modals/(.)profile/edit/faculty-photo-upload.tsx new file mode 100644 index 000000000..706683d58 --- /dev/null +++ b/app/[locale]/@modals/(.)profile/edit/faculty-photo-upload.tsx @@ -0,0 +1,231 @@ +'use client'; + +import { useEffect, useRef, useState } from 'react'; +import Image from 'next/image'; +import { AiOutlineLoading3Quarters } from 'react-icons/ai'; +import { FaCamera, FaUpload, FaUser } from 'react-icons/fa'; + +import { Button } from '~/components/buttons'; +import { env } from '~/lib/env/client'; +import { toast } from '~/lib/hooks'; +import { uploadMedia } from '~/server/actions/media-upload'; + +interface FacultyPhotoUploadProps { + facultyName: string; + employeeId: string; + facultyId: number; + onPhotoUploaded?: (url: string) => void; +} + +const ALLOWED_IMAGE_TYPES = ['image/jpeg', 'image/png', 'image/webp']; +const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB +const PHOTO_EXTENSIONS = ['jpg', 'png', 'webp'] as const; + +// Helper to construct potential photo URLs +function getPhotoUrls(employeeId: string, facultyId: number): string[] { + return PHOTO_EXTENSIONS.map( + (ext) => + `${env.NEXT_PUBLIC_AWS_S3_URL}/faculty-and-staff/${employeeId}/${facultyId}.${ext}` + ); +} + +export function FacultyPhotoUpload({ + facultyName, + employeeId, + facultyId, + onPhotoUploaded, +}: FacultyPhotoUploadProps) { + const [isUploading, setIsUploading] = useState(false); + const [previewUrl, setPreviewUrl] = useState(null); + const [isCheckingPhoto, setIsCheckingPhoto] = useState(true); + const fileInputRef = useRef(null); + + // Check for existing photo on mount by trying different extensions + useEffect(() => { + const checkExistingPhoto = async () => { + const photoUrls = getPhotoUrls(employeeId, facultyId); + + for (const url of photoUrls) { + try { + const response = await fetch(url, { method: 'HEAD' }); + if (response.ok) { + setPreviewUrl(url); + break; + } + } catch { + // Continue to next extension + } + } + setIsCheckingPhoto(false); + }; + + void checkExistingPhoto(); + }, [employeeId, facultyId]); + + const handleClick = () => { + fileInputRef.current?.click(); + }; + + const handleFileChange = async (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + // Validate file type + if (!ALLOWED_IMAGE_TYPES.includes(file.type)) { + toast({ + title: 'Invalid file type', + description: 'Please upload a JPEG, PNG, or WebP image.', + variant: 'error', + }); + return; + } + + // Show preview + const objectUrl = URL.createObjectURL(file); + setPreviewUrl(objectUrl); + + // Determine file extension + const extension = + file.type === 'image/jpg' + ? 'jpg' + : file.type === 'image/webp' + ? 'webp' + : 'png'; + + // Construct S3 path: isaac-s3-images/faculty-and-staff/{employee_id}/{faculty_id}.{ext} + const s3Path = `isaac-s3-images/faculty-and-staff/${employeeId}/${facultyId}.${extension}`; + + // Upload file + setIsUploading(true); + try { + const formData = new FormData(); + formData.append('file', file); + + const result = await uploadMedia(formData, s3Path, { + allowedTypes: ALLOWED_IMAGE_TYPES, + maxFileSize: MAX_FILE_SIZE, + }); + + if (result.success && result.url) { + toast({ + title: 'Success', + description: 'Profile photo updated successfully.', + variant: 'success', + }); + setPreviewUrl(result.url); + onPhotoUploaded?.(result.url); + } else { + toast({ + title: 'Error', + description: result.message, + variant: 'error', + }); + // Revert preview on error + URL.revokeObjectURL(objectUrl); + setPreviewUrl(null); + } + } catch (error) { + toast({ + title: 'Error', + description: 'Failed to upload photo. Please try again.', + variant: 'error', + }); + // Revert preview on error + URL.revokeObjectURL(objectUrl); + setPreviewUrl(null); + } finally { + setIsUploading(false); + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + + // Clean up object URL + return () => URL.revokeObjectURL(objectUrl); + }; + + return ( +
    + {/* Hidden file input */} + + + {/* Clickable photo container */} +
    { + if (e.key === 'Enter' || e.key === ' ') { + handleClick(); + } + }} + aria-label="Click to upload profile photo" + > + {/* Photo or fallback */} +
    + {isCheckingPhoto ? ( +
    + +
    + ) : previewUrl ? ( + {facultyName} + ) : ( +
    + +
    + )} + + {/* Overlay on hover */} +
    + {isUploading ? ( + + ) : ( + + )} +
    +
    +
    + + {/* Upload hint text */} +

    + {isUploading ? 'Uploading...' : 'Click to upload a new photo'} +

    + + {/* Alternative upload button */} + +
    + ); +} diff --git a/app/[locale]/@modals/(.)profile/edit/page.tsx b/app/[locale]/@modals/(.)profile/edit/page.tsx index e390976b8..bed54a20d 100644 --- a/app/[locale]/@modals/(.)profile/edit/page.tsx +++ b/app/[locale]/@modals/(.)profile/edit/page.tsx @@ -22,6 +22,7 @@ import { } from '~/server/db'; import { FacultyForm, FacultyPersonalDetailsForm } from './client-utils'; +import { FacultyPhotoUpload } from './faculty-photo-upload'; const facultyTables = { qualifications, @@ -51,6 +52,8 @@ export default async function Page({ .findFirst({ where: (faculty, { eq }) => eq(faculty.id, userId), columns: { + id: true, + employeeId: true, officeAddress: true, scopusId: true, linkedInId: true, @@ -61,6 +64,7 @@ export default async function Page({ with: { person: { columns: { + name: true, countryCode: true, telephone: true, alternateCountryCode: true, @@ -72,6 +76,9 @@ export default async function Page({ .then((result) => { if (!result) return null; return { + id: result.id, + employeeId: result.employeeId, + name: result.person.name, officeAddress: result.officeAddress, scopusId: result.scopusId ?? undefined, linkedInId: result.linkedInId ?? undefined, @@ -95,7 +102,19 @@ export default async function Page({ )} > - + +

    Edit Personal Details

    +
    + + {/* Photo Upload Section */} +
    + +
    + { + if (currentExtIndex < PHOTO_EXTENSIONS.length - 1) { + // Try next extension + setCurrentExtIndex((prev) => prev + 1); + } else { + // All extensions failed, use fallback + setUseFallback(true); + } + }; + + const imageSrc = useFallback + ? FALLBACK_IMAGE + : `${env.NEXT_PUBLIC_AWS_S3_URL}/faculty-and-staff/${employeeId}/${facultyId}.${PHOTO_EXTENSIONS[currentExtIndex]}`; + + if (fill) { + return ( + {alt} + ); + } + + return ( + {alt} + ); +} diff --git a/app/[locale]/faculty-and-staff/page.tsx b/app/[locale]/faculty-and-staff/page.tsx index 84e3aac28..d6a56e1d3 100644 --- a/app/[locale]/faculty-and-staff/page.tsx +++ b/app/[locale]/faculty-and-staff/page.tsx @@ -29,6 +29,7 @@ import { MobileFilters, PreserveParamsLink, } from './client-components'; +import { FacultyImage } from './faculty-image'; export default async function FacultyAndStaff({ params: { locale }, @@ -404,12 +405,13 @@ const FacultyList = async ({ className="flex gap-4 p-2 sm:p-3 md:p-4" href={`/${locale}/faculty-and-staff/${faculty.employeeId}`} > - {faculty.person.name}
    @@ -584,12 +586,13 @@ const StaffList = async ({ className="flex gap-4 p-2 sm:p-3 md:p-4" href={`/${locale}/faculty-and-staff/${staff.employeeId}`} > - {staff.person.name}
    diff --git a/app/[locale]/faculty-and-staff/utils.tsx b/app/[locale]/faculty-and-staff/utils.tsx index 06db22017..d11bc026b 100644 --- a/app/[locale]/faculty-and-staff/utils.tsx +++ b/app/[locale]/faculty-and-staff/utils.tsx @@ -46,6 +46,8 @@ import { students, } from '~/server/db'; +import { FacultyImage } from './faculty-image'; + // Contains the content of full Faculty Profile export async function FacultyOrStaffComponent({ children, @@ -237,12 +239,13 @@ export async function FacultyOrStaffComponent({ )} - 0
    • @@ -280,19 +283,13 @@ export async function FacultyOrStaffComponent({ {/* Faculty Image */}
      - 0
      {/* Faculty Intellectual Contribution counts */} diff --git a/server/actions/media-upload.ts b/server/actions/media-upload.ts new file mode 100644 index 000000000..c1996c2f5 --- /dev/null +++ b/server/actions/media-upload.ts @@ -0,0 +1,69 @@ +'use server'; + +import { env } from '~/lib/env/server'; +import { uploadFileToS3 } from '~/server/s3/upload'; + +const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB default + +interface UploadMediaOptions { + allowedTypes?: string[]; + maxFileSize?: number; +} + +const DEFAULT_OPTIONS: UploadMediaOptions = { + // Can update this list as needed + allowedTypes: ['image/jpeg', 'image/png', 'image/webp', 'application/pdf'], + maxFileSize: MAX_FILE_SIZE, +}; + +export async function uploadMedia( + formData: FormData, + s3Path: string, + options: UploadMediaOptions = {} +) { + const { allowedTypes, maxFileSize } = { ...DEFAULT_OPTIONS, ...options }; + + try { + const file = formData.get('file') as File; + + if (!file) { + return { success: false, message: 'No file provided' }; + } + + // Validate file type + if (allowedTypes && !allowedTypes.includes(file.type)) { + return { + success: false, + message: `Invalid file type. Allowed types: ${allowedTypes.join(', ')}`, + }; + } + + // Validate file size + if (maxFileSize && file.size > maxFileSize) { + const maxSizeMB = Math.round(maxFileSize / (1024 * 1024)); + return { + success: false, + message: `File too large. Maximum size is ${maxSizeMB}MB.`, + }; + } + + // Upload to S3 + await uploadFileToS3(file, s3Path); + + // Construct the public URL using environment variables + const publicUrl = `https://${env.AWS_PUBLIC_S3_NAME}.s3.${env.AWS_S3_REGION}.amazonaws.com/${s3Path}`; + + return { + success: true, + message: 'File uploaded successfully', + url: publicUrl, + path: s3Path, + }; + } catch (error) { + console.error('Error uploading media:', error); + return { + success: false, + message: 'Failed to upload file. Please try again.', + }; + } +} From 42422929210829291b6113a49309dd41dcbea7df Mon Sep 17 00:00:00 2001 From: Soumil Jain Date: Fri, 16 Jan 2026 00:41:42 +0530 Subject: [PATCH 12/73] Reusable Table Component replaced all tables with generic table component --- app/[locale]/academics/curricula/page.tsx | 113 ++- app/[locale]/academics/programmes/page.tsx | 127 ++-- .../institute/administration/page.tsx | 55 +- app/[locale]/institute/cells/iic/page.tsx | 122 ++- app/[locale]/institute/cells/ipr/page.tsx | 68 +- .../institute/hostels/[url_name]/page.tsx | 70 +- app/[locale]/institute/page.tsx | 70 +- .../sections/central-workshop/page.tsx | 258 ++++--- .../institute/sections/estate/page.tsx | 697 +++++++----------- .../institute/sections/health-centre/page.tsx | 152 ++-- .../library/library-committee/page.tsx | 47 +- .../membership-and-privileges/page.tsx | 38 +- .../institute/sections/library/page.tsx | 41 +- components/ui/generic-table.tsx | 45 +- package-lock.json | 10 + package.json | 1 + 16 files changed, 799 insertions(+), 1115 deletions(-) diff --git a/app/[locale]/academics/curricula/page.tsx b/app/[locale]/academics/curricula/page.tsx index 76a3918ac..f1c92ea69 100644 --- a/app/[locale]/academics/curricula/page.tsx +++ b/app/[locale]/academics/curricula/page.tsx @@ -2,22 +2,11 @@ export const revalidate = 300; import { count } from 'drizzle-orm'; -import Link from 'next/link'; import { Suspense } from 'react'; -import { FaExternalLinkAlt } from 'react-icons/fa'; -import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import Loading from '~/components/loading'; -import { PaginationWithLogic } from '~/components/pagination'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { courses, db } from '~/server/db'; @@ -49,36 +38,17 @@ export default async function Curricula({
      }> - - - - {text.code} - {text.title} - {text.major} - {text.credits} - {text.totalCredits} - {text.syllabus} - - - - - -
      +
      -
      ); } -const Courses = async ({ page }: { page: number }) => { - const courses = await db.query.courses.findMany({ +const Courses = async ({ page, locale }: { page: number; locale: string }) => { + const text = (await getTranslations(locale)).Curricula; + + const coursesData = await db.query.courses.findMany({ columns: { code: true, title: true }, with: { coursesToMajors: { @@ -90,42 +60,43 @@ const Courses = async ({ page }: { page: number }) => { with: { major: { columns: { name: true } } }, }, }, - limit: 10, - offset: (page - 1) * 10, }); - // console.log(courses); - - return courses.map(({ code, coursesToMajors, title }) => { - if (coursesToMajors.length === 0) { - return ( - - {code} - {title} - - ); - } - return coursesToMajors.map( - ({ lectureCredits, practicalCredits, tutorialCredits, major }, index) => ( - - {code} - {title} - {major.name} - {`${lectureCredits}-${tutorialCredits}-${practicalCredits}`} - - {lectureCredits + + // Transform data to flat structure for table + const tableData = coursesData.flatMap(({ code, coursesToMajors, title }) => + coursesToMajors.length === 0 + ? [{ code, title, major: '', credits: '', totalCredits: 0, syllabus: '' }] + : coursesToMajors.map( + ({ lectureCredits, practicalCredits, tutorialCredits, major }) => ({ + code, + title, + major: major.name, + credits: `${lectureCredits}-${tutorialCredits}-${practicalCredits}`, + totalCredits: + lectureCredits + practicalCredits + - Math.floor(tutorialCredits / 2)} - - - - - - ) - ); - }); + Math.floor(tutorialCredits / 2), + syllabus: `/en/academics/curricula/${code}`, + }) + ) + ); + + const headers = [ + { key: 'code', label: text.code }, + { key: 'title', label: text.title }, + { key: 'major', label: text.major }, + { key: 'credits', label: text.credits }, + { key: 'totalCredits', label: text.totalCredits }, + { key: 'syllabus', label: text.syllabus }, + ]; + + return ( + + ); }; diff --git a/app/[locale]/academics/programmes/page.tsx b/app/[locale]/academics/programmes/page.tsx index 2e6afd5af..924f70cdb 100644 --- a/app/[locale]/academics/programmes/page.tsx +++ b/app/[locale]/academics/programmes/page.tsx @@ -2,15 +2,8 @@ import { Fragment } from 'react'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; +import GenericTable from '~/components/ui/generic-table'; export default async function Programmes({ params: { locale }, @@ -137,6 +130,39 @@ export default async function Programmes({ }, ]; + // Prepare BTech table headers and data + const btechHeaders = [ + { key: 'name', label: text.discipline }, + { key: 'seats', label: text.noOfSeats }, + ]; + const btechData = btech; + + // Prepare MTech table headers and data (flattened) + const mtechHeaders = [ + { key: 'name', label: text.discipline }, + { key: 'specialization', label: text.secialization }, + ]; + const mtechData = mtech.flatMap((programme) => + programme.specializations.map((spec) => ({ + name: programme.name, + specialization: spec, + })) + ); + + // Prepare seat distribution table headers and data (flattened) + const seatDistHeaders = [ + { key: 'department', label: text.departmentAndSchools }, + { key: 'specialization', label: text.secialization }, + { key: 'seats', label: text.noOfSeats }, + ]; + const seatDistData = seatsMtech.flatMap((dept) => + dept.specialization.map((spec) => ({ + department: dept.department, + specialization: spec.name, + seats: spec.seats, + })) + ); + return ( <> @@ -151,22 +177,13 @@ export default async function Programmes({ {text.courseOfStudy} {text.btechAbout}


      - - - - {text.discipline} - {text.noOfSeats} - - - - {btech.map((programme, i) => ( - - {programme.name} - {programme.seats} - - ))} - -
      +
      @@ -182,28 +199,13 @@ export default async function Programmes({

      {text.secialization.toUpperCase()}

      - - - - {text.discipline} - {text.secialization} - - - - {mtech.map((programme, i) => ( - - {programme.name} - -
        - {programme.specializations.map((val, idx) => ( -
      • {val}
      • - ))} -
      -
      -
      - ))} -
      -
      +
      @@ -212,32 +214,13 @@ export default async function Programmes({ heading="h2" text={text.seatDistribution.toUpperCase()} /> - - - - {text.departmentAndSchools} - {text.secialization} - {text.noOfSeats} - - - - {seatsMtech.map((programme, i) => ( - - {programme.specialization.map((spec, index) => ( - - {index === 0 && ( - - {programme.department} - - )} - {spec.name} - {spec.seats} - - ))} - - ))} - -
      +
      ); diff --git a/app/[locale]/institute/administration/page.tsx b/app/[locale]/institute/administration/page.tsx index a87c5743c..2d580907c 100644 --- a/app/[locale]/institute/administration/page.tsx +++ b/app/[locale]/institute/administration/page.tsx @@ -12,20 +12,13 @@ import { MdEmail } from 'react-icons/md'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import ButtonGroup from '~/components/button-group'; -import Loading from '~/components/loading'; -import { - CardTitle, - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; // Fetches committee data from DB - cache for 1 hour export const revalidate = 3600; import { db } from '~/server/db'; +import GenericTable from '~/components/ui/generic-table'; +import Loading from '~/components/loading'; +import { CardTitle } from '~/components/ui'; export default async function Administration({ params: { locale }, @@ -108,21 +101,19 @@ export default async function Administration({ {text.composition} - - - - {text.sNo} - {text.name} - {text.servedAs} - - - - - -
      + eq(member.committeeType, 'senate'), + orderBy: (member, { asc }) => [asc(member.serial)], + })} + currentPage={1} + getCount={Promise.resolve([])} + />
    { - const members = await db.query.committeeMembers.findMany({ - where: (member, { eq }) => eq(member.committeeType, 'senate'), - orderBy: (member, { asc }) => [asc(member.serial)], - }); - - return members.map(({ serial, name, servingAs }, index) => ( - - {serial} - {name} - {servingAs} - - )); -}; const Deans = () => { const deans = [ { diff --git a/app/[locale]/institute/cells/iic/page.tsx b/app/[locale]/institute/cells/iic/page.tsx index bd12a2031..054372c6b 100644 --- a/app/[locale]/institute/cells/iic/page.tsx +++ b/app/[locale]/institute/cells/iic/page.tsx @@ -1,12 +1,5 @@ import Heading from '~/components/heading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; @@ -136,30 +129,25 @@ export default async function IICPage({ text={text.Institute.cells.iic.officeOrder.title} />
    - - - - - {text.Institute.cells.iic.officeOrder.srNo} - - - {text.Institute.cells.iic.officeOrder.responsibility} - - - {text.Institute.cells.iic.officeOrder.nameOfFaculty} - - - - - {officeOrder.map((order) => ( - - {order.sr_no} - {order.name_of_faculty} - {order.responsibility} - - ))} - -
    +
    {/* Activities */} @@ -172,49 +160,39 @@ export default async function IICPage({ /> {/* Past Activities */}
    - - - - - {text.Institute.cells.iic.activities.srNo} - - - {text.Institute.cells.iic.activities.pastActivities} - - - - - {activities.map((act) => ( - - {act.sr_no} - {act.past_activity} - - ))} - -
    +
    {/* Upcoming Activities */}
    - - - - - {text.Institute.cells.iic.activities.srNo} - - - {text.Institute.cells.iic.activities.upcomingActivities} - - - - - {activities.map((act) => ( - - {act.sr_no} - {act.past_activity} - - ))} - -
    +
    diff --git a/app/[locale]/institute/cells/ipr/page.tsx b/app/[locale]/institute/cells/ipr/page.tsx index 052e36072..e84420d20 100644 --- a/app/[locale]/institute/cells/ipr/page.tsx +++ b/app/[locale]/institute/cells/ipr/page.tsx @@ -9,14 +9,7 @@ import { type IconType } from 'react-icons/lib'; import { cn } from '~/lib/utils'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; @@ -68,73 +61,61 @@ export default async function IPR({ const advisoryCommittee = [ { - srNo: 1, name: 'Dr. R. P. Chauhan', designation: 'Professor', department: 'Physics', }, { - srNo: 2, name: 'Dr. R. P. Chauhan', designation: 'Assistant Professor', department: 'Physics', }, { - srNo: 3, name: 'Pratyush Prasoon Mishra', designation: 'Student', department: 'Computer Science', }, { - srNo: 4, name: 'Dr. Avijit Kumar Paul', designation: 'Assistant Professor', department: 'Chemistry', }, { - srNo: 5, name: 'Dr. Avijit Kumar Paul', designation: 'Assistant Professor', department: 'Chemistry', }, { - srNo: 6, name: 'Dr. R. P. Chauhan', designation: 'Professor', department: 'Physics', }, { - srNo: 7, name: 'Dr. Anjali Mehta', designation: 'Professor', department: 'Biotechnology', }, { - srNo: 8, name: 'Dr. Sumit Kapoor', designation: 'Assistant Professor', department: 'Mathematics', }, { - srNo: 9, name: 'Ishaan Arora', designation: 'Student', department: 'Electronics', }, { - srNo: 10, name: 'Dr. Nidhi Sharma', designation: 'Associate Professor', department: 'Mechanical', }, { - srNo: 11, name: 'Ananya Gupta', designation: 'Student', department: 'Information Technology', }, { - srNo: 12, name: 'Dr. Karan Sethi', designation: 'Assistant Professor', department: 'Civil', @@ -242,34 +223,25 @@ export default async function IPR({ text={text.Research.ipr.advisoryCommittee.title.toUpperCase()} />
    - - - - - {text.Research.ipr.advisoryCommittee.srNo} - - - {text.Research.ipr.advisoryCommittee.name} - - - {text.Research.ipr.advisoryCommittee.designation} - - - {text.Research.ipr.advisoryCommittee.department} - - - - - {advisoryCommittee.map((member) => ( - - {member.srNo} - {member.name} - {member.designation} - {member.department} - - ))} - -
    +
    {/* IP Policy */} diff --git a/app/[locale]/institute/hostels/[url_name]/page.tsx b/app/[locale]/institute/hostels/[url_name]/page.tsx index 2284813df..701da9e0e 100644 --- a/app/[locale]/institute/hostels/[url_name]/page.tsx +++ b/app/[locale]/institute/hostels/[url_name]/page.tsx @@ -4,14 +4,7 @@ import { notFound } from 'next/navigation'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { db, hostels } from '~/server/db'; @@ -146,32 +139,41 @@ export default async function Hostel({

    {index === 0 ? text.faculty : `${text.general} ${text.staff}`}

    - - - - {text.hostelsStaffTable.name} - {text.hostelsStaffTable.designation} - {text.hostelsStaffTable.hostelPost} - {text.contact} - {text.email} - - - - {hostel.hostelStaff.map(({ post, staff }) => ( - - {staff.person.name} - {staff.designation} - {post} - {staff.person.telephone} - - - {staff.person.email} - - - - ))} - -
    + { + const person = + 'faculty' in item + ? item.faculty?.person + : 'staff' in item + ? item.staff?.person + : undefined; + const designation = + 'faculty' in item + ? item.faculty?.designation + : 'staff' in item + ? item.staff?.designation + : undefined; + return { + name: person?.name, + designation, + post: item.post, + telephone: person?.telephone, + email: person?.email, + }; + })} + currentPage={1} + getCount={Promise.resolve([])} + /> ))} diff --git a/app/[locale]/institute/page.tsx b/app/[locale]/institute/page.tsx index 543077c02..21583f226 100644 --- a/app/[locale]/institute/page.tsx +++ b/app/[locale]/institute/page.tsx @@ -9,14 +9,7 @@ import { PiTreeStructureFill } from 'react-icons/pi'; import { BouncyArrowButton, Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; @@ -209,50 +202,23 @@ export default async function Institute({ id="nirf" text={text.nirf.title.toUpperCase()} /> - - - - {text.nirf.year} - {text.nirf.result} - {text.nirf.nirfCertificate} - {text.nirf.dataFile} - - - - {nirfData.map( - ( - { - year, - result, - nirfCertificate, - nirfCertificateLink, - dataFile, - dataFileLink, - }, - index - ) => ( - - {year} - {result} - - {nirfCertificateLink ? ( - {nirfCertificate} - ) : ( - nirfCertificate - )} - - - {dataFileLink ? ( - {dataFile} - ) : ( - dataFile - )} - - - ) - )} - -
    + ({ + ...row, + nirfCertificate: row.nirfCertificateLink + ? row.nirfCertificateLink + : row.nirfCertificate, + dataFile: row.dataFileLink ? row.dataFileLink : row.dataFile, + }))} + currentPage={1} + getCount={Promise.resolve([])} + /> {/* FUNDS */} diff --git a/app/[locale]/institute/sections/central-workshop/page.tsx b/app/[locale]/institute/sections/central-workshop/page.tsx index 8b6948b6e..f1b10c5d6 100644 --- a/app/[locale]/institute/sections/central-workshop/page.tsx +++ b/app/[locale]/institute/sections/central-workshop/page.tsx @@ -4,32 +4,100 @@ export const revalidate = 3600; import { Fragment, Suspense } from 'react'; import ImageHeader from '~/components/image-header'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; import { db } from '~/server/db'; +import GenericTable from '~/components/ui/generic-table'; + +/* ------------------------------------------------------------------ */ +/* Types */ +/* ------------------------------------------------------------------ */ + +interface TableRow { + name: string; + quantity: number; +} + +type CategoryKey = + | 'facilities' + | 'machineShop' + | 'productionShop' + | 'fittingShop' + | 'patternShop' + | 'foundryShop' + | 'weldingShop' + | 'camLabs'; + +interface CategoryText { + title: string; + sub?: string; + data: TableRow[]; + miscDetails?: string; +} + +interface StaffTableText { + staffTableTitle: { + name: string; + designation: string; + }; +} + +type CentralWorkshopText = { + title: string; + organization: string; + organizationSub: string; + organizationDetails: string[]; + services: string; + servicesSub: string; + servicesDetails: string[]; + tableTitle: { + sno: string; + name: string; + quantity: string; + }; + miscTitle: string; + equipmentDetails: string; + staffTitle: string; +} & Record & + StaffTableText; + +/* ------------------------------------------------------------------ */ +/* Page */ +/* ------------------------------------------------------------------ */ export default async function CentralWorkshop({ params: { locale }, }: { params: { locale: string }; }) { - const text = (await getTranslations(locale)).Section.CentralWorkshop; + const text = (await getTranslations(locale)).Section + .CentralWorkshop as unknown as CentralWorkshopText; - const section = (await db.query.sections.findFirst({ + const section = await db.query.sections.findFirst({ where: (section, { eq }) => eq(section.urlName, 'central-workshop'), - }))!; + }); + + if (!section) { + return null; + } + + const categories: readonly CategoryKey[] = [ + 'facilities', + 'machineShop', + 'productionShop', + 'fittingShop', + 'patternShop', + 'foundryShop', + 'weldingShop', + 'camLabs', + ]; + return ( <> +
    -

    {section?.aboutUs}

    +

    {section.aboutUs}

    +

    {text.organization}

    {text.organizationSub}
      @@ -37,6 +105,7 @@ export default async function CentralWorkshop({
    • {item}
    • ))}
    +

    {text.services}

    {text.servicesSub}
      @@ -44,94 +113,97 @@ export default async function CentralWorkshop({
    • {item}
    • ))}
    - {Object.freeze([ - 'facilities', - 'machineShop', - 'productionShop', - 'fittingShop', - 'patternShop', - 'foundryShop', - 'weldingShop', - 'camLabs', - ] as const).map((category, index) => ( - -

    {text[category].title}

    - {category === 'facilities' &&
    {text[category].sub}
    } - - - - {text.tableTitle.sno} - {text.tableTitle.name} - - {text.tableTitle.quantity} - - - - - {text[category].data.map(({ name, quantity }, index) => ( - - {index + 1} - {name} - {quantity} - - ))} - {'miscDetails' in text[category] && ( - <> - - {text.miscTitle} - - - - {/* @ts-expect-error: current ts version doesnt properly narrow type */} - {text[category].miscDetails as string} - - - - )} - -
    - {index == 0 && ( -

    {text.equipmentDetails}

    - )} -
    - ))} + + {categories.map((category, index) => { + const categoryText = text[category]; + + return ( + +

    {categoryText.title}

    + + {category === 'facilities' && categoryText.sub && ( +
    {categoryText.sub}
    + )} + + ({ + sno: i + 1, + name, + quantity, + }))} + currentPage={1} + getCount={Promise.resolve([])} + /> + + {categoryText.miscDetails && ( +
    +
    {text.miscTitle}
    +
    {categoryText.miscDetails}
    +
    + )} + + {index === 0 && ( +

    {text.equipmentDetails}

    + )} +
    + ); + })} +

    {text.staffTitle}

    - - - - {text.staffTableTitle.name} - {text.staffTableTitle.designation} - - - - - - Loading... - - - } - > - - - -
    + + Loading...}> + +
    ); } -const DelayedStaff = async ({ id }: { id: number }) => { +/* ------------------------------------------------------------------ */ +/* Staff Table */ +/* ------------------------------------------------------------------ */ + +const StaffTable = async ({ + sectionId, + text, +}: { + sectionId: number; + text: StaffTableText; +}) => { const staff = await db.query.staff.findMany({ columns: { id: true, designation: true }, - where: (staff, { eq }) => eq(staff.workingSectionId, id), - with: { person: { columns: { name: true, email: true, telephone: true } } }, + where: (staff, { eq }) => eq(staff.workingSectionId, sectionId), + with: { + person: { + columns: { + name: true, + email: true, + telephone: true, + }, + }, + }, }); - return staff.map(({ designation, person: { name } }, index) => ( - - {name} - {designation} - - )); + + return ( + ({ + name, + designation, + }))} + currentPage={1} + getCount={Promise.resolve([])} + /> + ); }; diff --git a/app/[locale]/institute/sections/estate/page.tsx b/app/[locale]/institute/sections/estate/page.tsx index 5648eaef6..974679ab7 100644 --- a/app/[locale]/institute/sections/estate/page.tsx +++ b/app/[locale]/institute/sections/estate/page.tsx @@ -6,18 +6,12 @@ import React from 'react'; import { Suspense } from 'react'; import { MdArticle } from 'react-icons/md'; +import { Table, TableBody, TableCell, TableRow } from '~/components/ui'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import Loading from '~/components/loading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; import { db } from '~/server/db'; @@ -35,220 +29,185 @@ export default async function Estate({ const committeeMembers = [ { - id: 1, name: 'Director, National Institute of Technology, Kurukshetra', position: 'Chairman', }, { - id: 2, name: 'Director (NITs), Deptt. of Higher Education, M.H.R.D, New Delhi', position: 'Member Nominated by the Central Government (MHRD), not below the rank of Director or Deputy Secretary', }, { - id: 3, name: 'Shri A K Singhal, Director General (Former), CPWD, New Delhi', position: 'Member Nominated by the Board of Governors', }, { - id: 4, name: 'Dean (Planning & Development) National Institute of Technology, Kurukshetra', position: 'Member Dean, Planning & Development or similar position', }, { - id: 5, name: 'Executive Engineer (Civil), CPWD, Karnal', position: 'Member Expert from the Civil Engineering Wing of the Central or State Government or any autonomous body of repute', }, { - id: 6, name: 'Executive Engineer (Electrical), CPWD, Karnal', position: 'Member Expert from the Electrical Engineering Wing of the Central or State Government or any autonomous body of repute', }, { - id: 7, name: 'Registrar National Institute of Technology, Kurukshetra', position: 'Member-Secretary', }, ]; const estateAffairsCommittee = [ { - id: 1, name: 'Dean (P&D)', position: 'Chairman', }, { - id: 2, name: 'Associate Dean P&D for (Estate & Construction)', position: 'Member', }, { - id: 3, name: 'Dr. Pratibha Aggarwal (Professor, CED)', position: 'Member', }, { - id: 4, name: 'Faculty I/C (Estate & Construction)', position: 'Member', }, { - id: 5, name: 'Faculty I/C (Elect. Mtc. & Telephone)', position: 'Member', }, { - id: 6, name: 'Assistant Engineer (Civil) SG-I', position: 'Member & Convener', }, { - id: 7, name: 'Assistant Engineer (Electrical)', position: 'Member', }, ]; const InspectionCommitteeMembers = [ - { id: 1, name: 'Dean (P&D)', position: 'Chairman' }, + { name: 'Dean (P&D)', position: 'Chairman' }, { - id: 2, name: 'Associate Dean P&D for (Estate & Construction)', position: 'Member', }, { - id: 3, name: 'One Professor/Associate Professor (Representative from Elect. Engg. Deptt.)', position: 'Member', }, { - id: 4, name: 'One Professor/Associate Professor (Representative from Civil Engg. Deptt.)', position: 'Member', }, - { id: 5, name: 'Faculty I/C (Estate & Construction)', position: 'Member' }, + { name: 'Faculty I/C (Estate & Construction)', position: 'Member' }, { - id: 6, name: 'Faculty I/C (Elect. Mtc. & Telephone)', position: 'Member', }, { - id: 7, name: 'Faculty I/C (Sanitation & Cleanliness)', position: 'Member', }, { - id: 8, name: 'Faculty I/C (Horticulture & Landscaping)', position: 'Member', }, - { id: 9, name: 'Assistant Engineer (Civil) SG-I', position: 'Member' }, + { name: 'Assistant Engineer (Civil) SG-I', position: 'Member' }, ]; const SpaceAllocationCommitteeMembers = [ - { id: 1, name: 'Dean (P&D)', position: 'Chairman' }, - { id: 2, name: 'Dean (Academic)', position: 'Member' }, - { id: 3, name: 'Dean (R&C)', position: 'Member' }, - { id: 4, name: 'Registrar', position: 'Member' }, + { name: 'Dean (P&D)', position: 'Chairman' }, + { name: 'Dean (Academic)', position: 'Member' }, + { name: 'Dean (R&C)', position: 'Member' }, + { name: 'Registrar', position: 'Member' }, { - id: 5, name: 'Associate Dean P&D for (Estate & Construction)', position: 'Member', }, - { id: 6, name: 'Prof. Surinder Deswal', position: 'Member' }, + { name: 'Prof. Surinder Deswal', position: 'Member' }, { - id: 7, name: 'Assistant Engineer (Civil) SG-I', position: 'Member & Convener', }, ]; const ProgressCommitteeMembers = [ - { id: 1, name: 'SE (Civil), CPWD', position: 'Member' }, - { id: 2, name: 'SE (Elect.), CPWD', position: 'Member' }, - { id: 3, name: 'Dean (P&D)', position: 'Member' }, + { name: 'SE (Civil), CPWD', position: 'Member' }, + { name: 'SE (Elect.), CPWD', position: 'Member' }, + { name: 'Dean (P&D)', position: 'Member' }, { - id: 4, name: 'Associate Dean P&D for (Estate & Construction)', position: 'Member', }, - { id: 5, name: 'Executive Engineer (Civil), CPWD', position: 'Member' }, - { id: 6, name: 'Executive Engineer (Elect.), CPWD', position: 'Member' }, + { name: 'Executive Engineer (Civil), CPWD', position: 'Member' }, + { name: 'Executive Engineer (Elect.), CPWD', position: 'Member' }, { - id: 7, name: 'Other concerned officials of the Estate Section of NIT, Kurukshetra', position: 'Member', }, ]; const LicensingCommitteeMembers = [ - { id: 1, name: 'Associate Dean (P&D) for E & C', position: 'Chairman' }, - { id: 2, name: 'JR (GA & Legal)', position: 'Member' }, - { id: 3, name: 'AR (Accounts)', position: 'Member' }, - { id: 4, name: 'President or his Nominee (NITTAK)', position: 'Member' }, + { name: 'Associate Dean (P&D) for E & C', position: 'Chairman' }, + { name: 'JR (GA & Legal)', position: 'Member' }, + { name: 'AR (Accounts)', position: 'Member' }, + { name: 'President or his Nominee (NITTAK)', position: 'Member' }, { - id: 5, name: 'President or his Nominee Non-Teaching (Karamchari Sangh)', position: 'Member', }, { - id: 6, name: 'Assistant Engineer (Civil) SG-I', position: 'Member & Convener', }, ]; const HouseAllotmentCommitteeMembers = [ { - id: 1, name: 'Dr. Dinesh Khanduja', position: 'Professor, Mechanical Engg. Deptt.', role: 'Chairman', }, { - id: 2, name: 'Sh. K K Sharma', position: 'Associate Professor, Elect. Engg. Deptt.', role: 'Member', }, { - id: 3, name: 'Dr. Rajender Kumar', position: 'Assistant Professor, ECE Deptt.', role: 'Member', }, { - id: 4, name: '-', position: 'Representative of Teaching Association', role: 'Member', }, { - id: 5, name: '-', position: 'Executive Engineer/ Assistant Engineer (Civil) SG-I', role: 'Member-Secretary', }, ]; const HouseAllotmentCommitteeMembers2 = [ - { id: 1, name: 'Registrar', position: '', role: 'Chairman' }, + { name: 'Registrar', position: '', role: 'Chairman' }, { - id: 2, name: 'Sh. Sanjay Keswani', position: 'Technical Assistant SG-I, Elect. Engg. Deptt.', role: 'Member', }, { - id: 3, name: 'Smt. Shashi Bala', position: 'Superintendent SG-II, Academic Section', role: 'Member', }, { - id: 4, name: 'Representative of Non-Teaching Association', position: '', role: 'Member', }, { - id: 5, name: 'Assistant Engineer (Civil) SG-I', position: '', role: 'Member & Secretary', @@ -256,73 +215,64 @@ export default async function Estate({ ]; const areaDetails = [ { - id: 1, description: 'Total Area of the Institute (in Acres)', value: '292', }, { - id: 2, description: 'Total Area of the Institute (in Sqm)', value: '11,79,607.60', }, - { id: 3, description: 'Built up Area (in Sqm)', value: '1,15,941.39' }, + { description: 'Built up Area (in Sqm)', value: '1,15,941.39' }, ]; const infrastructureDetails = [ { - id: 1, details: 'NIT Main Gates Along Kirmich road towards KUK (in Sqm)', area: '31.14', }, - { id: 2, details: 'Creche (in Sqm)', area: '854.12' }, + { details: 'Creche (in Sqm)', area: '854.12' }, ]; const academicAreaDetails = [ { - id: 1, details: 'Office Space: Golden Jubilee Administrative Building Old Admn. Block Estate Store', area: '11,969.00', }, - { id: 2, details: 'Electrical Block', area: '5363.99' }, - { id: 3, details: 'Mechanical Deptt', area: '1545.27' }, - { id: 4, details: 'Civil Engg. Deptt', area: '1558.73' }, - { id: 5, details: 'AM block', area: '1805.41' }, - { id: 6, details: 'AB block/ ED Cell', area: '1245.42' }, + { details: 'Electrical Block', area: '5363.99' }, + { details: 'Mechanical Deptt', area: '1545.27' }, + { details: 'Civil Engg. Deptt', area: '1558.73' }, + { details: 'AM block', area: '1805.41' }, + { details: 'AB block/ ED Cell', area: '1245.42' }, { - id: 7, details: 'Workshop Equip. Bldg./ Old MBA & MCA & Class rooms at 1st Floor', area: '5155.11', }, - { id: 8, details: 'MBA & MCA Deptt', area: '3503.66' }, - { id: 9, details: '12 No. Lecture Theatre Complex', area: '3298.24' }, - { id: 10, details: 'Electronics & Comm. Engg. Deptt', area: '1809.32' }, - { id: 11, details: '06 No. Lecture Theatre Complex', area: '1239.68' }, - { id: 12, details: 'Computer Engg. Deptt', area: '2104.51' }, - { id: 13, details: 'Mechanical Engg. Block', area: '6240.00' }, - { id: 14, details: 'Work shop Complex Part-1', area: '4145.20' }, - { id: 15, details: 'Examination Hall', area: '1878.81' }, + { details: 'MBA & MCA Deptt', area: '3503.66' }, + { details: '12 No. Lecture Theatre Complex', area: '3298.24' }, + { details: 'Electronics & Comm. Engg. Deptt', area: '1809.32' }, + { details: '06 No. Lecture Theatre Complex', area: '1239.68' }, + { details: 'Computer Engg. Deptt', area: '2104.51' }, + { details: 'Mechanical Engg. Block', area: '6240.00' }, + { details: 'Work shop Complex Part-1', area: '4145.20' }, + { details: 'Examination Hall', area: '1878.81' }, ]; const hostelAreaData = [ { - id: 1, name: '10 No. Boys Hostel 1-10', type: 'Boys Hostel', area: 139724.89, }, { - id: 2, name: '04 No. Girls Hostel 1-4', type: 'Girls Hostel', area: 40467.18, }, { - id: 3, name: 'Old Barrack Quarter', type: 'Bearer Barrack Quarter', area: 1700.07, }, { - id: 4, name: 'New Barrack Quarter', type: 'Bearer Barrack Quarter', area: 1637.55, @@ -456,35 +406,30 @@ export default async function Estate({ ]; const hostelData = [ { - id: 1, name: 'Old Boys Hostels (Three Seats per Room in Hostel 1, 2, 3 & Single Seat per Room in Hostel No. 4, 5)', type: 'UG Boys Hostel', capacity: 250, quantity: 5, }, { - id: 2, name: 'New 350 Seaters Boys Hostel', type: 'UG Boys Hostel', capacity: 350, quantity: 2, }, { - id: 3, name: 'Visvesvaraya Hostel (1000 Capacity Mega Hostel, Single Seater)', type: 'UG Boys Hostel', capacity: 1000, quantity: 1, }, { - id: 4, name: 'Old PG Hostels for Boys (150 Capacity, Single Seater)', type: 'PG Boys Hostel', capacity: 150, quantity: 1, }, { - id: 5, name: 'New PG Hostel for Boys (350 Capacity, Single Seater)', type: 'PG Boys Hostel', capacity: 350, @@ -493,35 +438,30 @@ export default async function Estate({ ]; const girlsHostelData = [ { - id: 1, name: 'Old Girls Hostels (120 Capacity)', type: 'Girls Hostel', capacity: 120, quantity: 1, }, { - id: 2, name: 'Seats per Room: 1 Seater (42 Rooms), 2 Seater (6 Rooms), 3 Seater (22 Rooms)', type: 'Girls Hostel', capacity: 'Varies', quantity: 1, }, { - id: 3, name: 'New 200 Seaters Girls Hostel (200 Capacity, Single Seater)', type: 'Girls Hostel', capacity: 200, quantity: 1, }, { - id: 4, name: 'New 300 Seaters Girls Hostel (300 Capacity, Single Seater)', type: 'Girls Hostel', capacity: 300, quantity: 1, }, { - id: 5, name: 'New 600 Seaters Girls Hostel (600 Capacity, Single Seater)', type: 'Girls Hostel', capacity: 600, @@ -530,121 +470,101 @@ export default async function Estate({ ]; const residentialAreaData = [ { - id: 1, type: 'Director', plinthArea: '2250 + Office(GF) 1098 (FF)', numberOfHouses: '01', }, { - id: 2, type: 'BT type houses', plinthArea: '2250.00 + parking (stilt floor)', numberOfHouses: '20', }, { - id: 3, type: 'BA type houses', plinthArea: '2250.00', numberOfHouses: '06', }, { - id: 4, type: 'BB type houses (ss)', plinthArea: '1820.00', numberOfHouses: '08', }, { - id: 5, type: 'BB type houses (ds)', plinthArea: '1700.00 + garage', numberOfHouses: '08', }, { - id: 6, type: 'BC type houses', plinthArea: '1660.00 + garage', numberOfHouses: '06', }, { - id: 7, type: 'CT type houses', plinthArea: '1800.00 + parking (stilt floor)', numberOfHouses: '20', }, { - id: 8, type: 'CA type houses', plinthArea: '1550.00 + garage', numberOfHouses: '13', }, { - id: 9, type: 'CB type houses (ss)', plinthArea: '1380.00', numberOfHouses: '04', }, { - id: 10, type: 'CB type houses (ds)', plinthArea: '1400.00 + garage', numberOfHouses: '05', }, { - id: 11, type: 'CC type houses', plinthArea: '1300.00', numberOfHouses: '12', }, { - id: 12, type: 'AD(A) type houses', plinthArea: '1394.00', numberOfHouses: '04', }, { - id: 13, type: 'AD(B) type houses', plinthArea: '1020.00', numberOfHouses: '02', }, { - id: 14, type: 'DA type houses', plinthArea: '1020.00', numberOfHouses: '15', }, { - id: 15, type: 'DB type houses', plinthArea: '922.00', numberOfHouses: '34+34=68', }, { - id: 16, type: 'E type houses', plinthArea: '840.00', numberOfHouses: '12+12=24', }, { - id: 17, type: 'F type houses', plinthArea: '670.00', numberOfHouses: '38+38=76', }, { - id: 18, type: 'MF type houses', plinthArea: '670.00', numberOfHouses: '02', }, { - id: 19, type: 'G type houses', plinthArea: '450.00', numberOfHouses: '60+30=90', }, { - id: 20, type: 'MG type houses', plinthArea: '450.00', numberOfHouses: '02', @@ -652,45 +572,39 @@ export default async function Estate({ ]; const supportingFacilitiesData = [ { - id: 1, facility: 'Bank & Post Office at 1st Floor of NITK Market', area: '418.29 Sqm', }, { - id: 2, facility: 'Senate Hall cum Restaurant/ Canteen', area: '1532.43 Sqm', }, { - id: 3, facility: 'Institute Guest House', area: '717.75 + 1108.22 = 1825.97 Sqm', }, - { id: 4, facility: 'Faculty House (9000.00 Sqft.)', area: '1096.88 Sqm' }, + { facility: 'Faculty House (9000.00 Sqft.)', area: '1096.88 Sqm' }, { - id: 5, facility: 'HT/LT Sub-station: 1. Sub-station Near CCN Department, 2. Sub-station Near Hostel No.2, 3. Sub-station Near Gol Canteen, 4. Sub-station Near Main Gate Kirmich Road Side', area: '146.74 Sqm, 235.60 Sqm, 52.71 Sqm, 95.42 Sqm', }, - { id: 6, facility: 'Library', area: '4021.13 Sqm' }, - { id: 7, facility: 'Computer Block (CCN)', area: '564.46 Sqm' }, - { id: 8, facility: 'NCC & NSS Offices', area: '20 Sqm' }, - { id: 9, facility: 'Student Activity Centre', area: '487.53 Sqm' }, - { id: 10, facility: 'Sports Complex', area: '547.71 Sqm' }, - { id: 11, facility: 'Alumni Centre', area: '20 Sqm' }, - { id: 12, facility: 'NIT Market', area: '836.57 Sqm' }, - { id: 13, facility: 'Health Centre', area: '225.75 + 238.43 = 464.18 Sqm' }, + { facility: 'Library', area: '4021.13 Sqm' }, + { facility: 'Computer Block (CCN)', area: '564.46 Sqm' }, + { facility: 'NCC & NSS Offices', area: '20 Sqm' }, + { facility: 'Student Activity Centre', area: '487.53 Sqm' }, + { facility: 'Sports Complex', area: '547.71 Sqm' }, + { facility: 'Alumni Centre', area: '20 Sqm' }, + { facility: 'NIT Market', area: '836.57 Sqm' }, + { facility: 'Health Centre', area: '225.75 + 238.43 = 464.18 Sqm' }, { - id: 14, facility: 'Open Air-Theatre & Parking', area: '1560 + 780 = 2340 Sqm', }, - { id: 15, facility: 'Jubilee Hall', area: '455.21 Sqm' }, - { id: 16, facility: 'Swimming Pool', area: '2909.74 Sqm' }, - { id: 17, facility: '02 No. Students Parking', area: '2450.00 Sqm' }, + { facility: 'Jubilee Hall', area: '455.21 Sqm' }, + { facility: 'Swimming Pool', area: '2909.74 Sqm' }, + { facility: '02 No. Students Parking', area: '2450.00 Sqm' }, { - id: 18, facility: 'Gol Canteen (for research purpose/ CPWD offices)', area: '635.44 Sqm', }, @@ -786,24 +700,15 @@ export default async function Estate({ text={text.headings[1].toUpperCase()} /> }> - - - - S.No - Name - Position - - - - {committeeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - - ))} - -
    +
    @@ -816,169 +721,102 @@ export default async function Estate({ />

    {text.subheadings[0].toUpperCase()}

    }> - - - - S.No - Name - - Position - - - - {estateAffairsCommittee.map((member) => ( - - {member.id} - {member.name} - - {member.position} - - ))} - -
    +

    INSPECTION COMMITTEE (IC)

    }> - - - - S.No - Name - Position - - - - {InspectionCommitteeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - - ))} - -
    +

    {text.subheadings[1].toUpperCase()}

    }> - - - - S.No - Name - Position - - - - {SpaceAllocationCommitteeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - - ))} - -
    +

    {text.subheadings[2].toUpperCase()}

    }> - - - - S.No - Designation - Position - - - - {ProgressCommitteeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - - ))} - -
    +

    {text.subheadings[3].toUpperCase()}

    }> - - - - S.No - Designation - Position - - - - {LicensingCommitteeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - - ))} - -
    +

    {text.subheadings[4].toUpperCase()}

    }> - - - - S.No - Name - Position - Role - - - - {HouseAllotmentCommitteeMembers.map((member) => ( - - {member.id} - {member.name} - {member.position} - {member.role} - - ))} - -
    +

    {text.subheadings[5].toUpperCase()}

    }> - - - - S.No - Name - Position - Role - - - - {HouseAllotmentCommitteeMembers2.map((member) => ( - - {member.id} - {member.name} - {member.position} - {member.role} - - ))} - -
    +
    @@ -990,181 +828,143 @@ export default async function Estate({ text={text.headings[3].toUpperCase()} /> }> - - - {areaDetails.map((detail) => ( - - {detail.description} - {detail.value} - - ))} - -
    +

    {text.subheadings[6].toUpperCase()}

    }> - - - {infrastructureDetails.map((infrastructure) => ( - - {infrastructure.details} - {infrastructure.area} - - ))} - -
    +

    {text.subheadings[7].toUpperCase()}

    }> - - - - S.No. - Details - Area (in Sqm) - - - - {academicAreaDetails.map((areaDetail) => ( - - {areaDetail.id} - {areaDetail.details} - {areaDetail.area} - - ))} - -
    + ({ + details: item.details, + area: item.area, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[8].toUpperCase()}

    }> - - - - S.No - Name - Type - Area (in Sqm) - - - - {hostelAreaData.map((item) => ( - - {item.id} - {item.name} - {item.type} - {item.area} - - ))} - -
    + ({ + name: item.name, + type: item.type, + area: item.area, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[9].toUpperCase()}

    }> - - - - S.No - Name - Type - Capacity - Quantity - - - - {hostelData.map((item) => ( - - {item.id} - {item.name} - {item.type} - {item.capacity} - {item.quantity} - - ))} - -
    + ({ + name: item.name, + type: item.type, + capacity: item.capacity, + quantity: item.quantity, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[10].toUpperCase()}

    }> - - - - S.No - Name - Type - Capacity - Quantity - - - - {girlsHostelData.map((item) => ( - - {item.id} - {item.name} - {item.type} - {item.capacity} - {item.quantity} - - ))} - -
    + ({ + name: item.name, + type: item.type, + capacity: item.capacity, + quantity: item.quantity, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[11].toUpperCase()}

    }> - - - - S.No - Type/Category - Plinth Area (Sqft.) - No. of Houses - - - - {residentialAreaData.map((item) => ( - - {item.id} - {item.type} - {item.plinthArea} - {item.numberOfHouses} - - ))} - -
    + ({ + type: item.type, + plinthArea: item.plinthArea, + numberOfHouses: item.numberOfHouses, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[12].toUpperCase()}

    }> - - - - S.No - Facility - Area (Sqm) - - - - {supportingFacilitiesData.map((item) => ( - - {item.id} - {item.facility} - {item.area} - - ))} - -
    + ({ + facility: item.facility, + area: item.area, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    @@ -1177,45 +977,42 @@ export default async function Estate({ />

    {text.subheadings[13].toUpperCase()}

    - - - {text.project.completed.slice(0).map((project, index) => ( - - {project} - - ))} - -
    + ({ + project, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[14].toUpperCase()}

    - - - {text.project.ongoing.slice(0).map((project, index) => ( - - {project} - - ))} - -
    + ({ + project, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />

    {text.subheadings[15].toUpperCase()}

    - - - {text.project.future.slice(0).map((project, index) => ( - - {project} - - ))} - -
    + ({ + project, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    diff --git a/app/[locale]/institute/sections/health-centre/page.tsx b/app/[locale]/institute/sections/health-centre/page.tsx index 3115d4459..35b84d464 100644 --- a/app/[locale]/institute/sections/health-centre/page.tsx +++ b/app/[locale]/institute/sections/health-centre/page.tsx @@ -8,14 +8,7 @@ import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import { getTranslations } from '~/i18n/translations'; import { db } from '~/server/db'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import Loading from '~/components/loading'; export default async function HealthCentre({ @@ -487,29 +480,23 @@ export default async function HealthCentre({ text={text.headings.timings.toUpperCase()} /> }> - - - - {text.timings.day} - Timings - - - - {timings.days.map((entry, idx) => ( - - {entry.day} - - {entry.timings.map((timing, idx) => ( -
    - {`${timing.from} - ${timing.to}`} - {timing.label ? `${timing.label} ` : ''} -
    - ))} -
    -
    - ))} -
    -
    + ({ + day: entry.day, + timings: entry.timings + .map( + (timing) => + `${timing.from} - ${timing.to}${timing.label ? ` ${timing.label}` : ''}` + ) + .join(', '), + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    @@ -522,49 +509,37 @@ export default async function HealthCentre({ /> }>

    {text.staff.officers}

    - - - - {text.staff.sr} - {text.staff.name} - {text.staff.designation} - {text.staff.phone} - - - - {medicalOfficersData.map((entry, idx) => ( - - {entry.srNo} - {entry.name} - {entry.role} - {entry.tel} - - ))} - -
    + ({ + name, + role, + tel, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    }>

    {text.staff.other}

    - - - - {text.staff.sr} - {text.staff.name} - {text.staff.designation} - {text.staff.phone} - - - - {medicalStaffData.map((entry, idx) => ( - - {entry.srNo} - {entry.name} - {entry.designation} - {entry.phone} - - ))} - -
    + ({ + name, + designation, + phone, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    @@ -718,27 +693,20 @@ export default async function HealthCentre({ heading="h4" text={text.facilities.hospitals.toUpperCase()} /> - - - - - {text.hospitals.sr} - {text.hospitals.name} - {text.hospitals.field} - {text.hospitals.contact} - - - - {hospitalData.map((entry, idx) => ( - - {entry.srNo} - {entry.name} - {entry.field} - {entry.phone} - - ))} - -
    + ({ + name, + field, + phone, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    diff --git a/app/[locale]/institute/sections/library/library-committee/page.tsx b/app/[locale]/institute/sections/library/library-committee/page.tsx index 2fc072bdb..05408375d 100644 --- a/app/[locale]/institute/sections/library/library-committee/page.tsx +++ b/app/[locale]/institute/sections/library/library-committee/page.tsx @@ -2,14 +2,7 @@ import { Suspense } from 'react'; import Heading from '~/components/heading'; import Loading from '~/components/loading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { db } from '~/server/db'; @@ -47,26 +40,24 @@ export default async function libraryCommittee({ id="library-committee" /> }> - - - - {text.srNo} - {text.name} - {text.generalDesignation} - {text.libraryCommitteeDesignation} - - - - {libraryCommitteeData.map((entry, index) => ( - - {index} - {entry.faculty.person.name} - {entry.faculty.designation} - {entry.libraryCommitteeDesignation} - - ))} - -
    + ({ + srNo: index + 1, + name: entry.faculty.person.name, + generalDesignation: entry.faculty.designation, + libraryCommitteeDesignation: entry.libraryCommitteeDesignation, + }))} + currentPage={1} + getCount={Promise.resolve([])} + />
    ); diff --git a/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx b/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx index e12915b91..b72317648 100644 --- a/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx +++ b/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx @@ -1,12 +1,5 @@ import Heading from '~/components/heading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; export default async function MembershipAndPrivileges({ @@ -62,25 +55,16 @@ export default async function MembershipAndPrivileges({
    {text.membershipPrivilegesText}
    - - - - Category of Members - No. of Books Loan - Period of Loan - - - - - {LoanTableData.map((entry, index) => ( - - {entry.category} - {entry.noOfBooksLoan} - {entry.periodOfLoan} - - ))} - -
    +
    diff --git a/app/[locale]/institute/sections/library/page.tsx b/app/[locale]/institute/sections/library/page.tsx index cb4bf243e..93fc46d4d 100644 --- a/app/[locale]/institute/sections/library/page.tsx +++ b/app/[locale]/institute/sections/library/page.tsx @@ -8,14 +8,7 @@ import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import Loading from '~/components/loading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; @@ -270,27 +263,17 @@ export default async function Library({ id="contact-us" /> }> - - - - {text.contactUs.name} - {text.contactUs.designation} - {text.contactUs.phoneNumber} - {text.contactUs.email} - - - - - {contactUsData.map((entry, index) => ( - - {entry.name} - {entry.designation} - {entry.phoneNumber} - {entry.email} - - ))} - -
    +
    diff --git a/components/ui/generic-table.tsx b/components/ui/generic-table.tsx index 1e45b0a59..b724c017f 100644 --- a/components/ui/generic-table.tsx +++ b/components/ui/generic-table.tsx @@ -1,6 +1,8 @@ 'use client'; import { Suspense } from 'react'; +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; import { Table, @@ -23,15 +25,28 @@ interface GenericTableProps> { tableData: T[]; currentPage: number; itemsPerPage?: number; - getCount: Promise<{ count: number }[]>; // changed type + getCount: Promise<{ count: number }[]>; } -export function GenericTable>({ +// Helper function to check if a value is a valid URL (absolute or relative) +const isValidUrl = (value: unknown): boolean => { + if (typeof value !== 'string') return false; + + // Check for absolute URLs + try { + const url = new URL(value); + return url.protocol === 'http:' || url.protocol === 'https:'; + } catch { + // Check for relative URLs (starts with /) + return value.startsWith('/'); + } +}; + +export default function GenericTable>({ headers, tableData, currentPage, itemsPerPage = 10, - getCount, }: GenericTableProps) { const startIndex = (currentPage - 1) * itemsPerPage; const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); @@ -67,11 +82,25 @@ export function GenericTable>({ > {startIndex + rowIndex + 1} - {headers.map((header, colIndex) => ( - - {String(item[header.key])} - - ))} + {headers.map((header, colIndex) => { + const cellValue = item[header.key]; + const isLink = isValidUrl(cellValue); + + return ( + + {isLink ? ( + + + + ) : ( + String(cellValue) + )} + + ); + })} ))} diff --git a/package-lock.json b/package-lock.json index ec84f0fac..a173ae1ab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "framer-motion": "^11.0.5", "jiti": "^1.21.0", "jotai": "^2.11.1", + "lucide-react": "^0.562.0", "negotiator": "^0.6.3", "next": "^14.2.26", "next-auth": "^4.24.5", @@ -7832,6 +7833,15 @@ "node": ">=10" } }, + "node_modules/lucide-react": { + "version": "0.562.0", + "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz", + "integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==", + "license": "ISC", + "peerDependencies": { + "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", diff --git a/package.json b/package.json index 000cd4c7d..bb62dec29 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "framer-motion": "^11.0.5", "jiti": "^1.21.0", "jotai": "^2.11.1", + "lucide-react": "^0.562.0", "negotiator": "^0.6.3", "next": "^14.2.26", "next-auth": "^4.24.5", From 4f829bcfe080b03302b37095a6c0dbf9d80905da Mon Sep 17 00:00:00 2001 From: Arnav Sharma <145358467+ArnavSharma005@users.noreply.github.com> Date: Fri, 16 Jan 2026 00:52:50 +0530 Subject: [PATCH 13/73] Reusable Notifications component This pull request refactors the notifications display logic across several pages by introducing a new reusable `NotificationsPanel` component. The main goal is to centralize and streamline how notifications are fetched, filtered, and rendered, reducing code duplication and improving maintainability. The panel supports flexible filtering and customization, and is now used in place of previous ad-hoc implementations in the Academics and Notifications pages. **Notifications UI Refactor and Componentization** * Introduced a new `NotificationsPanel` component that encapsulates all logic for fetching, filtering (by category, club, date), and displaying notifications, with support for loading states and customization options. This component replaces previous inline implementations and is now the single source for notifications UI. * Updated the Academics (`app/[locale]/academics/page.tsx`) and Notifications (`app/[locale]/notifications.tsx`) pages to use the new `NotificationsPanel`, removing their local notification list logic and related imports, and simplifying their main render logic. ([app/[locale]/academics/page.tsxL5-L29](diffhunk://#diff-bb42d3e07d6a4b2f5a35dc4e6210318de482ab7f0c0a2bf19580b1cedf742a82L5-L29), [app/[locale]/academics/page.tsxL48-R41](diffhunk://#diff-bb42d3e07d6a4b2f5a35dc4e6210318de482ab7f0c0a2bf19580b1cedf742a82L48-R41), [app/[locale]/academics/page.tsxL80-R78](diffhunk://#diff-bb42d3e07d6a4b2f5a35dc4e6210318de482ab7f0c0a2bf19580b1cedf742a82L80-R78), [app/[locale]/academics/page.tsxL298-L339](diffhunk://#diff-bb42d3e07d6a4b2f5a35dc4e6210318de482ab7f0c0a2bf19580b1cedf742a82L298-L339), [app/[locale]/notifications.tsxL2-L12](diffhunk://#diff-fc4ab4f58418a1ce1ca032d258b139c979709ec0e41edf5494c8c0b99a31b6ecL2-L12), [app/[locale]/notifications.tsxL72-L148](diffhunk://#diff-fc4ab4f58418a1ce1ca032d258b139c979709ec0e41edf5494c8c0b99a31b6ecL72-L148)) **Code Cleanup and Consistency** * Removed now-unused components and helper functions (`NotificationsList` and related database queries/grouping logic) from both the Academics and Notifications pages, as this functionality is now handled by the new panel. ([app/[locale]/academics/page.tsxL298-L339](diffhunk://#diff-bb42d3e07d6a4b2f5a35dc4e6210318de482ab7f0c0a2bf19580b1cedf742a82L298-L339), [app/[locale]/notifications.tsxL72-L148](diffhunk://#diff-fc4ab4f58418a1ce1ca032d258b139c979709ec0e41edf5494c8c0b99a31b6ecL72-L148)) * Updated imports in dependent files (such as the Hostels page) to use the new default export for `NotificationsList`, ensuring consistency with the refactor. ([app/[locale]/institute/hostels/page.tsxL16-R16](diffhunk://#diff-11a346a0b3b637deeabb5eb45cbb54a281cb3a311e358726adc923b9057c9c80L16-R16)) --- app/[locale]/academics/page.tsx | 96 ++-------- app/[locale]/header.tsx | 8 +- app/[locale]/institute/hostels/page.tsx | 33 +--- app/[locale]/notifications.tsx | 98 ++-------- app/[locale]/page.tsx | 6 +- .../clubs/[display_name]/page.tsx | 50 +---- components/notifications-panel.tsx | 174 ++++++++++++++++++ 7 files changed, 220 insertions(+), 245 deletions(-) create mode 100644 components/notifications-panel.tsx diff --git a/app/[locale]/academics/page.tsx b/app/[locale]/academics/page.tsx index 62bb77fc6..83d4559a7 100644 --- a/app/[locale]/academics/page.tsx +++ b/app/[locale]/academics/page.tsx @@ -2,31 +2,24 @@ export const revalidate = 300; import Link from 'next/link'; -import { Suspense } from 'react'; import { FaTrophy } from 'react-icons/fa6'; import { HiMiniBeaker } from 'react-icons/hi2'; -import { MdBadge, MdOutlineKeyboardArrowRight } from 'react-icons/md'; +import { MdBadge } from 'react-icons/md'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import Loading from '~/components/loading'; -import { ScrollArea } from '~/components/ui'; +import NotificationsPanel from '~/components/notifications-panel'; import { getTranslations } from '~/i18n/translations'; -import { cn, groupBy } from '~/lib/utils'; -import { db, type notifications as notificationsSchema } from '~/server/db'; +import { cn } from '~/lib/utils'; import { getS3Url } from '~/server/s3'; export default async function Academics({ params: { locale }, - searchParams, }: { params: { locale: string }; - searchParams: Record; }) { const text = (await getTranslations(locale)).Academics; - const currentCategory = - (searchParams.notificationCategory as string | undefined) ?? 'academics'; return ( <> @@ -45,7 +38,7 @@ export default async function Academics({

    {text.aboutDetail}

    {/*
      @@ -77,37 +70,12 @@ export default async function Academics({ ))}
    */} -
    - -
      - } key={currentCategory}> - - -
    -
    - -
    - -
    -
    +

    @@ -294,46 +262,4 @@ export default async function Academics({
    ); -} - -const NotificationsList = async ({ - category, - locale, -}: { - category: (typeof notificationsSchema.category.enumValues)[number]; - locale: string; -}) => { - const notifications = ( - await db.query.notifications.findMany({ - where: (notification, { eq }) => eq(notification.category, category), - }) - ).map((notification) => ({ - ...notification, - createdAt: notification.createdAt.toLocaleString(locale, { - dateStyle: 'long', - numberingSystem: locale === 'hi' ? 'deva' : 'roman', - }), - })); - - return Array.from(groupBy(notifications, 'createdAt')).map( - ([createdAt, notifications], index) => ( -
  • -
    {createdAt as string}
    -
      - {notifications.map(({ id, title }, index) => ( -
    • - - -

      {title}

      - -
    • - ))} -
    -
    -
  • - ) - ); -}; +} \ No newline at end of file diff --git a/app/[locale]/header.tsx b/app/[locale]/header.tsx index 3a43f33cc..6e6094bff 100644 --- a/app/[locale]/header.tsx +++ b/app/[locale]/header.tsx @@ -107,13 +107,7 @@ export default async function Header({ locale }: { locale: string }) { href: '/academics/scholarships', description: 'Learn about scholarships, eligibility, and application details.', - }, - { - title: 'Academic Notifications', - href: '/academics/notifications', - description: - 'Stay updated with the latest academic announcements and deadlines.', - }, + } ], }, { label: text.faculty, href: 'faculty-and-staff' }, diff --git a/app/[locale]/institute/hostels/page.tsx b/app/[locale]/institute/hostels/page.tsx index 89ab2f2c7..484d53a41 100644 --- a/app/[locale]/institute/hostels/page.tsx +++ b/app/[locale]/institute/hostels/page.tsx @@ -7,14 +7,13 @@ import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import Loading from '~/components/loading'; -import { Card, ScrollArea } from '~/components/ui'; +import NotificationsPanel from '~/components/notifications-panel'; +import { Card } from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; import { cn, groupBy } from '~/lib/utils'; import { db } from '~/server/db'; import { getS3Url } from '~/server/s3'; -import { NotificationsList } from '../../notifications'; - // Fetches hostel data from DB - cache for 1 hour export const revalidate = 3600; @@ -56,27 +55,13 @@ export default async function Hostels({ href="#notification" glyphDirection={'rtl'} /> -
    - -
      - } key={'hostel'}> - - -
    -
    -
    +
    + +
    - {getKeys(text.categories).map((category, index) => ( + {notificationCategories.map((category, index) => (
  • -
    - -
      - } key={currentCategory}> - - -
    -
    - -
    - -
    -
    + ); -} - -export const NotificationsList = async ({ - category, - locale, -}: { - category: (typeof notificationsSchema.category.enumValues)[number]; - locale: string; -}) => { - const notifications = ( - await db.query.notifications.findMany({ - where: (notification, { eq }) => eq(notification.category, category), - }) - ).map((notification) => ({ - ...notification, - createdAt: notification.createdAt.toLocaleString(locale, { - dateStyle: 'long', - numberingSystem: locale === 'hi' ? 'deva' : 'roman', - }), - })); - - return Array.from(groupBy(notifications, 'createdAt')).map( - ([createdAt, notifications], index) => ( -
  • -
    {createdAt as string}
    -
      - {notifications.map(({ id, title }, index) => ( -
    • - - -

      {title}

      - -
    • - ))} -
    -
    -
  • - ) - ); -}; +} \ No newline at end of file diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index a764ee5c5..8367b4246 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -2,7 +2,7 @@ import Image from 'next/image'; import { BsLinkedin } from 'react-icons/bs'; import { MdEmail, MdPhone } from 'react-icons/md'; -import Notifications from '~/app/notifications'; +import Notifications, { type NotificationCategory } from '~/app/notifications'; import { Button } from '~/components/buttons'; import { AutoplayCarousel, @@ -14,7 +14,7 @@ import { import Heading from '~/components/heading'; import MessageCard from '~/components/message-card'; import { getTranslations } from '~/i18n/translations'; -import { type events, type notifications } from '~/server/db'; +import { type events } from '~/server/db'; import Events from './events'; @@ -27,7 +27,7 @@ export default async function Home({ }: { params: { locale: string }; searchParams: { - notificationCategory?: (typeof notifications.category.enumValues)[number]; + notificationCategory?: NotificationCategory; eventsCategory?: | (typeof events.category.enumValues)[number] | 'recents' diff --git a/app/[locale]/student-activities/clubs/[display_name]/page.tsx b/app/[locale]/student-activities/clubs/[display_name]/page.tsx index 820bd4ffb..963a9f575 100644 --- a/app/[locale]/student-activities/clubs/[display_name]/page.tsx +++ b/app/[locale]/student-activities/clubs/[display_name]/page.tsx @@ -14,6 +14,7 @@ import { getS3Url } from '~/server/s3'; import { GalleryCarousel } from '~/components/carousels'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; +import NotificationsPanel from '~/components/notifications-panel'; import { Card, CardContent, @@ -313,29 +314,6 @@ export default async function Club({ }, ]; - const dummyNotifications = [ - { - content: 'Meeting scheduled for all club members at 5 PM.', - updatedAt: new Date('2024-09-01T14:30:00Z'), - }, - { - content: 'New event: Coding Marathon on 12th September. Register now!', - updatedAt: new Date('2024-09-02T10:15:00Z'), - }, - { - content: 'Reminder: Submit your project reports by Friday.', - updatedAt: new Date('2024-09-03T08:45:00Z'), - }, - { - content: "Club membership renewals are open. Don't forget to renew!", - updatedAt: new Date('2024-09-04T11:00:00Z'), - }, - { - content: 'Workshop on Android development scheduled for next week.', - updatedAt: new Date('2024-09-05T09:30:00Z'), - }, - ]; - const dummyClubData = { howToJoinUs: 'To join our club, simply fill out the membership form available on our website or attend our weekly meetings held every Friday at 5 PM in the main auditorium.', @@ -386,25 +364,13 @@ export default async function Club({ id="notifications" text={text.Club.notification.toUpperCase()} /> -
    - - - - Note - Date - - - - {/* {club?.clubNotifications.map((note, i) => ( */} - {dummyNotifications.map((note, i) => ( - - {note.content} - {note.updatedAt.toDateString()} - - ))} - -
    -
    + {/* Events */} + +
      + } key={filterKey}> + + +
    +
    + + {showViewAll && ( +
    + +
    + )} +
    + ); +} + +interface NotificationsListProps { + locale: string; + category?: NotificationCategory; + clubId?: number; + startDate?: Date; + endDate?: Date; +} + +const NotificationsList = async ({ + locale, + category, + clubId, + startDate, + endDate, +}: NotificationsListProps) => { + const notifications = ( + await db.query.notifications.findMany({ + where: (notification, { eq, and, gte, lte }) => { + const conditions = []; + + if (category) { + conditions.push(eq(notification.category, category)); + } + if (clubId !== undefined) { + conditions.push(eq(notification.clubId, clubId)); + } + if (startDate) { + conditions.push(gte(notification.createdAt, startDate)); + } + if (endDate) { + conditions.push(lte(notification.createdAt, endDate)); + } + + return conditions.length > 0 ? and(...conditions) : undefined; + }, + orderBy: (notification, { desc }) => [desc(notification.createdAt)], + }) + ).map((notification) => ({ + ...notification, + createdAt: notification.createdAt.toLocaleString(locale, { + dateStyle: 'long', + numberingSystem: locale === 'hi' ? 'deva' : 'roman', + }), + })); + + if (notifications.length === 0) { + return ( +
  • + No notifications found +
  • + ); + } + + return Array.from(groupBy(notifications, 'createdAt')).map( + ([createdAt, notifications], index) => ( +
  • +
    + {createdAt as string} +
    +
      + {notifications.map(({ id, title }, index) => ( +
    • + + +

      + {title} +

      + +
    • + ))} +
    +
    +
  • + ) + ); +}; From 1a9f1265a32633715cba5e51690fa42d081ecc8f Mon Sep 17 00:00:00 2001 From: Aryawart Kathpal <132134276+Aryawart-kathpal@users.noreply.github.com> Date: Fri, 16 Jan 2026 13:57:36 +0530 Subject: [PATCH 14/73] Notifications page and Modal --- app/[locale]/academics/page.tsx | 4 +- app/[locale]/institute/hostels/page.tsx | 2 +- app/[locale]/noticeboard/page.tsx | 9 - app/[locale]/notifications.tsx | 11 +- app/[locale]/notifications/DateRangeForm.tsx | 191 +++++++++++ app/[locale]/notifications/MobileFilters.tsx | 249 ++++++++++++++ app/[locale]/notifications/MultiCheckbox.tsx | 194 +++++++++++ .../notifications/NotificationModal.tsx | 156 +++++++++ .../notifications/NotificationsList.tsx | 153 +++++++++ app/[locale]/notifications/SearchInput.tsx | 48 +++ app/[locale]/notifications/page.tsx | 303 ++++++++++++++++++ .../clubs/[display_name]/page.tsx | 4 +- .../notification-item-with-modal.tsx | 40 +++ .../notifications-panel.tsx | 64 ++-- components/ui/slider.tsx | 7 +- i18n/en.ts | 42 ++- i18n/hi.ts | 42 ++- i18n/translations.ts | 38 ++- server/actions/index.ts | 1 + server/actions/notifications.ts | 157 +++++++++ server/db/schema/notifications.schema.ts | 115 +++++-- 21 files changed, 1751 insertions(+), 79 deletions(-) delete mode 100644 app/[locale]/noticeboard/page.tsx create mode 100644 app/[locale]/notifications/DateRangeForm.tsx create mode 100644 app/[locale]/notifications/MobileFilters.tsx create mode 100644 app/[locale]/notifications/MultiCheckbox.tsx create mode 100644 app/[locale]/notifications/NotificationModal.tsx create mode 100644 app/[locale]/notifications/NotificationsList.tsx create mode 100644 app/[locale]/notifications/SearchInput.tsx create mode 100644 app/[locale]/notifications/page.tsx create mode 100644 components/notifications/notification-item-with-modal.tsx rename components/{ => notifications}/notifications-panel.tsx (72%) create mode 100644 server/actions/notifications.ts diff --git a/app/[locale]/academics/page.tsx b/app/[locale]/academics/page.tsx index 83d4559a7..4e736f5ee 100644 --- a/app/[locale]/academics/page.tsx +++ b/app/[locale]/academics/page.tsx @@ -9,7 +9,7 @@ import { MdBadge } from 'react-icons/md'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import NotificationsPanel from '~/components/notifications-panel'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; import { getS3Url } from '~/server/s3'; @@ -73,7 +73,7 @@ export default async function Academics({ diff --git a/app/[locale]/institute/hostels/page.tsx b/app/[locale]/institute/hostels/page.tsx index 484d53a41..5a6b7da69 100644 --- a/app/[locale]/institute/hostels/page.tsx +++ b/app/[locale]/institute/hostels/page.tsx @@ -7,7 +7,7 @@ import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import Loading from '~/components/loading'; -import NotificationsPanel from '~/components/notifications-panel'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { Card } from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; import { cn, groupBy } from '~/lib/utils'; diff --git a/app/[locale]/noticeboard/page.tsx b/app/[locale]/noticeboard/page.tsx deleted file mode 100644 index a38fd5717..000000000 --- a/app/[locale]/noticeboard/page.tsx +++ /dev/null @@ -1,9 +0,0 @@ -import { WorkInProgressStatus } from '~/components/status'; - -export default function NoticeBoard({ - params: { locale }, -}: { - params: { locale: string }; -}) { - return ; -} diff --git a/app/[locale]/notifications.tsx b/app/[locale]/notifications.tsx index adfd29016..29beb0315 100644 --- a/app/[locale]/notifications.tsx +++ b/app/[locale]/notifications.tsx @@ -1,12 +1,17 @@ import Link from 'next/link'; import Heading from '~/components/heading'; -import NotificationsPanel from '~/components/notifications-panel'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; import { getS3Url } from '~/server/s3'; -const notificationCategories = ['academic', 'tender', 'workshop', 'recruitment'] as const; +const notificationCategories = [ + 'academic', + 'tender', + 'workshop', + 'recruitment', +] as const; export type NotificationCategory = (typeof notificationCategories)[number]; export default async function Notifications({ @@ -75,4 +80,4 @@ export default async function Notifications({ ); -} \ No newline at end of file +} diff --git a/app/[locale]/notifications/DateRangeForm.tsx b/app/[locale]/notifications/DateRangeForm.tsx new file mode 100644 index 000000000..5e93d7438 --- /dev/null +++ b/app/[locale]/notifications/DateRangeForm.tsx @@ -0,0 +1,191 @@ +'use client'; + +import React from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +import { cn } from '~/lib/utils'; +import { Slider } from '~/components/ui'; + +interface DateRangeFormText { + startDate: string; + endDate: string; + day: string; + month: string; + year: string; +} + +export function DateRangeForm({ + start, + end, + compact = false, + text, +}: { + locale: string; + categories?: string[]; + departments?: string[]; + query?: string; + start?: string; + end?: string; + compact?: boolean; + text?: DateRangeFormText; +}) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const [yearRange, setYearRange] = React.useState([ + start ? new Date(start).getFullYear() : 2000, + end ? new Date(end).getFullYear() : 2025, + ]); + + const startDate = start ? new Date(start) : undefined; + const endDate = end ? new Date(end) : undefined; + + const [startDay, setStartDay] = React.useState(startDate?.getDate() ?? 1); + const [startMonth, setStartMonth] = React.useState( + startDate ? startDate.getMonth() + 1 : 1 + ); + const [endDay, setEndDay] = React.useState(endDate?.getDate() ?? 1); + const [endMonth, setEndMonth] = React.useState( + endDate ? endDate.getMonth() + 1 : 1 + ); + + // Apply filters to URL - only called on explicit user actions + const applyFilters = React.useCallback( + (newYearRange?: number[]) => { + const params = new URLSearchParams(searchParams); + const years = newYearRange ?? yearRange; + + const startVal = + years[0] && startMonth && startDay + ? `${years[0]}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}` + : undefined; + const endVal = + years[1] && endMonth && endDay + ? `${years[1]}-${String(endMonth).padStart(2, '0')}-${String(endDay).padStart(2, '0')}` + : undefined; + + if (startVal) { + params.set('start', startVal); + } else { + params.delete('start'); + } + + if (endVal) { + params.set('end', endVal); + } else { + params.delete('end'); + } + + const newUrl = `${pathname}?${params.toString()}`; + router.push(newUrl, { scroll: false }); + }, + [yearRange, startDay, startMonth, endDay, endMonth, searchParams, pathname, router] + ); + + return ( +
    + {/* Year Range Slider */} +
    +
    +
    + {yearRange[0]} +
    +
    + {yearRange[1]} +
    +
    + { + setYearRange(value); + applyFilters(value); + }} + className="w-full" + /> +
    + + {/* Start Date */} +
    + +
    + setStartDay(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setStartMonth(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setYearRange([+e.target.value, yearRange[1]])} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> +
    +
    + + {/* End Date */} +
    + +
    + setEndDay(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setEndMonth(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setYearRange([yearRange[0], +e.target.value])} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> +
    +
    +
    + ); +} diff --git a/app/[locale]/notifications/MobileFilters.tsx b/app/[locale]/notifications/MobileFilters.tsx new file mode 100644 index 000000000..34bf54101 --- /dev/null +++ b/app/[locale]/notifications/MobileFilters.tsx @@ -0,0 +1,249 @@ +'use client'; + +import { createPortal } from 'react-dom'; +import React, { useEffect, useRef, useState } from 'react'; +import Link from 'next/link'; +import { FaTimes } from 'react-icons/fa'; +import { MdFilterList } from 'react-icons/md'; + +import { ScrollArea } from '~/components/ui/scroll-area'; +import { cn } from '~/lib/utils'; + +import { DateRangeForm } from './DateRangeForm'; +import { MultiCheckbox } from './MultiCheckbox'; + +interface Dept { + id: number; + name: string; + urlName: string; +} + +type Cat = string; + +interface MobileFiltersProps { + locale: string; + categories: Cat[]; + departments: string[]; + departmentRows: Dept[]; + categoryOptions: readonly string[]; + query: string; + start?: string; + end?: string; + text: { + filters: string; + filterBy: string; + clearAllFilters: string; + filter: { + date: string; + category: string; + department: string; + startDate: string; + endDate: string; + day: string; + month: string; + year: string; + }; + categories: Record; + }; + className?: string; +} + +export function MobileFilters({ + locale, + categories, + departments, + departmentRows, + categoryOptions, + query, + start, + end, + text, + className, +}: MobileFiltersProps) { + const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + const panelRef = useRef(null); + + // Handle close with animation + const handleClose = () => { + setIsAnimating(false); + setTimeout(() => setOpen(false), 300); + }; + + // Trigger animation after open + useEffect(() => { + if (open) { + requestAnimationFrame(() => setIsAnimating(true)); + } + }, [open]); + + // Lock body scroll while open + useEffect(() => { + const prev = document.body.style.overflow; + if (open) document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prev; + }; + }, [open]); + + // Close on outside click + useEffect(() => { + if (!open) return; + + document.body.classList.add('overflow-hidden'); + return () => { + document.body.classList.remove('overflow-hidden'); + }; + }, [open]); + + // Calculate active filters count (including date filters) + const dateFiltersCount = (start ? 1 : 0) + (end ? 1 : 0); + const activeFiltersCount = + categories.length + departments.length + dateFiltersCount; + + return ( +
    + {/* Filter button */} + + + {/* Backdrop */} +
    +
    +
    + + {open && + createPortal( + , + document.body + )} +
    + ); +} diff --git a/app/[locale]/notifications/MultiCheckbox.tsx b/app/[locale]/notifications/MultiCheckbox.tsx new file mode 100644 index 000000000..90703d8d4 --- /dev/null +++ b/app/[locale]/notifications/MultiCheckbox.tsx @@ -0,0 +1,194 @@ +'use client'; + +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; + +import { cn } from '~/lib/utils'; +import { ScrollArea } from '~/components/ui'; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from '~/components/inputs'; + +export function MultiCheckbox({ + param, + options, + selected, + locale, + textMap, + select = false, +}: { + param: string; + options: readonly string[]; + selected: string[]; + locale: string; + textMap: Record; + select?: boolean; +}) { + const searchParams = useSearchParams(); + + const isAllSelected = selected.length === 0; + + // Sort options: selected ones first, then unselected + const sortedOptions = [...options].sort((a, b) => { + const aSelected = selected.includes(a); + const bSelected = selected.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + + const getUpdatedValues = (option: string) => { + return selected.includes(option) + ? selected.filter((s) => s !== option) + : [...selected, option]; + }; + + const buildLocalHref = (updates: Record) => { + const params = new URLSearchParams(searchParams); + + Object.entries(updates).forEach(([k, v]) => { + if (v === undefined || (Array.isArray(v) && v.length === 0)) { + params.delete(k); + return; + } + + if (Array.isArray(v)) { + params.delete(k); + v.forEach((item) => { + if (item) params.append(k, String(item)); + }); + } else { + params.set(k, String(v)); + } + }); + + const qs = params.toString(); + return `/${locale}/notifications${qs ? `?${qs}` : ''}`; + }; + + return select ? ( + + + All + +
    +
    + {sortedOptions.map((opt) => ( +
    + + + {textMap[opt] ?? opt} + +
    + ))} + + + ) : ( + +
      + {/* All Option */} +
    1. + +
      +
      + +
      + All +
      + +
    2. + {sortedOptions.map((opt) => { + const isChecked = selected.includes(opt); + return ( +
    3. + +
      +
      + +
      + + {textMap[opt] ?? opt} + +
      + +
    4. + ); + })} +
    +
    + ); +} diff --git a/app/[locale]/notifications/NotificationModal.tsx b/app/[locale]/notifications/NotificationModal.tsx new file mode 100644 index 000000000..1f8efa2eb --- /dev/null +++ b/app/[locale]/notifications/NotificationModal.tsx @@ -0,0 +1,156 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { MdCalendarToday, MdOpenInNew } from 'react-icons/md'; + +import { Dialog, DialogContent, ScrollArea } from '~/components/ui'; +import Loading from '~/components/loading'; +import { + getNotificationById, + type NotificationDetails, +} from '~/server/actions/notifications'; + +interface NotificationModalProps { + notificationId: number | null; + onClose: () => void; + locale: string; +} + +export function NotificationModal({ + notificationId, + onClose, + locale, +}: NotificationModalProps) { + const [notification, setNotification] = useState( + null + ); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (notificationId === null) { + setNotification(null); + return; + } + + setIsLoading(true); + getNotificationById(notificationId) + .then((data) => { + setNotification(data); + }) + .catch((error) => { + console.error('Failed to fetch notification:', error); + setNotification(null); + }) + .finally(() => { + setIsLoading(false); + }); + }, [notificationId]); + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString(locale, { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + }; + + // Extract filename from URL for document button label (without extension) + const getDocumentName = (url: string, index: number) => { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop(); + if (filename) { + // Decode URI and remove extension + const decoded = decodeURIComponent(filename); + const nameWithoutExt = decoded.replace(/\.[^/.]+$/, ''); + return nameWithoutExt || decoded; + } + } catch { + // If URL parsing fails, use generic name + } + return `Document ${index + 1}`; + }; + + return ( + !open && onClose()} + > + + {isLoading ? ( +
    + +
    + ) : notification ? ( + <> + {/* Header with date and close button */} +
    + + + {formatDate(notification.createdAt)} + +
    + + {/* Title */} +

    + {notification.title} +

    + + {/* Content */} + {notification.content && ( +
    + +

    + {notification.content} +

    +
    +
    + )} + + {/* Documents */} + {notification.documents.length > 0 && ( +
    +
    2 ? 'h-32 sm:h-auto' : '' + } ${notification.documents.length > 6 ? 'sm:h-52' : ''}`} + > + +
    + {notification.documents.map((doc, index) => ( + + + {getDocumentName(doc, index)} + + + + ))} +
    +
    +
    +
    + )} + + ) : ( +
    +

    Notification not found

    +
    + )} +
    +
    + ); +} diff --git a/app/[locale]/notifications/NotificationsList.tsx b/app/[locale]/notifications/NotificationsList.tsx new file mode 100644 index 000000000..5349e7832 --- /dev/null +++ b/app/[locale]/notifications/NotificationsList.tsx @@ -0,0 +1,153 @@ +'use client'; + +import { useCallback, useEffect, useRef, useState } from 'react'; +import { MdOutlineKeyboardArrowRight } from 'react-icons/md'; + +import { groupBy } from '~/lib/utils'; +import Loading from '~/components/loading'; +import { + loadMoreNotifications, + type LoadMoreParams, + type NotificationItem, +} from '~/server/actions/notifications'; + +import { NotificationModal } from './NotificationModal'; + +interface NotificationsListProps { + initialItems: NotificationItem[]; + initialCursor: string | null; + initialHasMore: boolean; + locale: string; + filterParams: LoadMoreParams; + text: { + noNotificationsFound: string; + noMoreNotifications: string; + }; +} + +export function NotificationsList({ + initialItems, + initialCursor, + initialHasMore, + locale, + filterParams, + text, +}: NotificationsListProps) { + const [items, setItems] = useState(initialItems); + const [cursor, setCursor] = useState(initialCursor); + const [hasMore, setHasMore] = useState(initialHasMore); + const [isLoading, setIsLoading] = useState(false); + const [selectedId, setSelectedId] = useState(null); + + const observerRef = useRef(null); + const loadMoreRef = useRef(null); + + // Reset when filter params or initial data change + useEffect(() => { + setItems(initialItems); + setCursor(initialCursor); + setHasMore(initialHasMore); + }, [initialItems, initialCursor, initialHasMore]); + + const loadMore = useCallback(async () => { + if (isLoading || !hasMore || !cursor) return; + + setIsLoading(true); + try { + const result = await loadMoreNotifications({ + ...filterParams, + cursor, + }); + + setItems((prev) => [...prev, ...result.items]); + setCursor(result.nextCursor); + setHasMore(result.hasMore); + } catch (error) { + console.error('Failed to load more notifications:', error); + } finally { + setIsLoading(false); + } + }, [cursor, hasMore, isLoading, filterParams]); + + // Intersection Observer for infinite scroll + useEffect(() => { + if (observerRef.current) observerRef.current.disconnect(); + + observerRef.current = new IntersectionObserver( + (entries) => { + if (entries[0]?.isIntersecting && hasMore && !isLoading) { + void loadMore(); + } + }, + { threshold: 0.1, rootMargin: '100px' } + ); + + if (loadMoreRef.current) { + observerRef.current.observe(loadMoreRef.current); + } + + return () => observerRef.current?.disconnect(); + }, [hasMore, isLoading, loadMore]); + + if (!items.length) { + return ( +

    + {text.noNotificationsFound} +

    + ); + } + + // Format and group items + const formattedItems = items.map((n) => ({ + ...n, + createdAtStr: new Date(n.createdAt).toLocaleDateString(locale, { + dateStyle: 'long', + numberingSystem: locale === 'hi' ? 'deva' : 'roman', + }), + })); + + const grouped = Array.from(groupBy(formattedItems, 'createdAtStr')); + + return ( + <> + {grouped.map(([createdAtStr, group], idx) => ( +
    +
    + {createdAtStr} +
    +
      + {group.map((n) => ( +
    • + +
    • + ))} +
    +
    +
    + ))} + + {/* Load more trigger */} +
    + {isLoading && } + {!hasMore && items.length > 0 && ( +

    + {text.noMoreNotifications} +

    + )} +
    + + {/* Notification Modal */} + setSelectedId(null)} + locale={locale} + /> + + ); +} diff --git a/app/[locale]/notifications/SearchInput.tsx b/app/[locale]/notifications/SearchInput.tsx new file mode 100644 index 000000000..ba9787a63 --- /dev/null +++ b/app/[locale]/notifications/SearchInput.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname,useRouter, useSearchParams } from 'next/navigation'; +import { MdSearch } from 'react-icons/md'; + +export function SearchInput({ + defaultValue, + placeholder, +}: { + defaultValue?: string; + placeholder: string; +}) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [query, setQuery] = useState(defaultValue ?? ''); + + useEffect(() => { + const timer = setTimeout(() => { + const params = new URLSearchParams(searchParams); + + if (query.trim()) { + params.set('q', query.trim()); + } else { + params.delete('q'); + } + + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, 300); + + return () => clearTimeout(timer); + }, [query, searchParams, pathname, router]); + + return ( +
    + + setQuery(e.target.value)} + placeholder={placeholder} + className="bg-white w-full rounded-md border border-neutral-300 px-3 py-2 pl-10 text-sm placeholder:text-neutral-400 focus:border-primary-700 focus:outline-none focus:ring-1 focus:ring-primary-700" + /> +
    + ); +} diff --git a/app/[locale]/notifications/page.tsx b/app/[locale]/notifications/page.tsx new file mode 100644 index 000000000..a0f79cfed --- /dev/null +++ b/app/[locale]/notifications/page.tsx @@ -0,0 +1,303 @@ +import Link from 'next/link'; +import React from 'react'; +import { desc } from 'drizzle-orm'; + +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; +import { cn } from '~/lib/utils'; +import ImageHeader from '~/components/image-header'; +import { Button } from '~/components/buttons'; +import { ScrollArea } from '~/components/ui'; +import { notifications as notificationsSchema } from '~/server/db'; +import { type NotificationItem } from '~/server/actions/notifications'; + +import { DateRangeForm } from './DateRangeForm'; +import { MobileFilters } from './MobileFilters'; +import { MultiCheckbox } from './MultiCheckbox'; +import { NotificationsList } from './NotificationsList'; +import { SearchInput } from './SearchInput'; + +type Cat = (typeof notificationsSchema.category.enumValues)[number]; + +const INITIAL_BATCH_SIZE = 20; + +interface PageSearchParams { + q?: string; + category?: string | string[]; + department?: string | string[]; + start?: string; + end?: string; +} + +export default async function NotificationsPage({ + params: { locale }, + searchParams, +}: { + params: { locale: string }; + searchParams: PageSearchParams; +}) { + const text = (await getTranslations(locale)).Notifications; + + // Normalize multi-select params + const categories = toArray(searchParams.category).filter(Boolean) as Cat[]; + const departments = toArray(searchParams.department).filter(Boolean); + const startDate = parseDate(searchParams.start); + const endDate = parseDate(searchParams.end); + const query = (searchParams.q ?? '').trim().toLowerCase(); + + // Fetch departments (names + urlName) for filter list + const departmentRows = await db.query.departments.findMany({ + columns: { id: true, name: true, urlName: true }, + }); + + // Get department IDs for filtering + const deptIds = departments.length + ? departmentRows + .filter((d) => departments.includes(d.urlName)) + .map((d) => d.id) + : []; + + // Build base query - fetch only initial batch + let raw = await db.query.notifications.findMany({ + where: (n, { and, gte, lte }) => + and( + startDate ? gte(n.createdAt, startDate) : undefined, + endDate ? lte(n.createdAt, endDate) : undefined + ), + orderBy: (n) => [desc(n.createdAt)], + limit: INITIAL_BATCH_SIZE + 1, // +1 to check if there are more + }); + + // Category filter (multi) + if (categories.length) { + raw = raw.filter((n) => categories.includes(n.category as Cat)); + } + + // Department filter (multi via foreign key, if departmentId present) + if (deptIds.length) { + raw = raw.filter((n) => n.departmentId && deptIds.includes(n.departmentId)); + } + + // Text search (title and content) + if (query) { + raw = raw.filter( + (n) => + n.title.toLowerCase().includes(query.toLowerCase()) || + n.content?.toLowerCase().includes(query.toLowerCase()) + ); + } + + // Check if there are more items + const hasMore = raw.length > INITIAL_BATCH_SIZE; + const initialItems = hasMore ? raw.slice(0, INITIAL_BATCH_SIZE) : raw; + const initialCursor = hasMore + ? initialItems[initialItems.length - 1]?.createdAt.toISOString() ?? null + : null; + + // Serialize for client component + const serializedItems: NotificationItem[] = initialItems.map((n) => ({ + id: n.id, + title: n.title, + category: n.category, + createdAt: n.createdAt.toISOString(), + })); + + // Build filter params for the client component + const filterParams = { + categories: categories.length ? categories : undefined, + departmentIds: deptIds.length ? deptIds : undefined, + start: searchParams.start, + end: searchParams.end, + query: query || undefined, + }; + + return ( + <> + +
    + {/* Desktop Sidebar - hidden on mobile */} + + {/* Main Content */} +
    + {/* Search + Mobile Filters */} + + + + {/* Mobile Filters Button - shows on < xl */} +
    + +
    +
    + + {/* Notifications List */} +
    + +
    +
    +
    + + ); +} + +/* ---------------------- Filters Components ---------------------- */ + +function FilterSection({ + label, + children, + viewAllHref, +}: { + label: string; + children: React.ReactNode; + viewAllHref?: string; + locale?: string; +}) { + return ( +
    +
    +

    {label}

    + {viewAllHref && ( + + View All + + )} +
    + {children} +
    + ); +} +/* ---------------------- Helpers ---------------------- */ +function toArray(v: string | string[] | undefined): string[] { + return Array.isArray(v) ? v : v ? [v] : []; +} + +function parseDate(d?: string) { + if (!d) return undefined; + const date = new Date(d); + return isNaN(date.getTime()) ? undefined : date; +} + +function buildHref(locale: string, updates: Record): string { + const params = new URLSearchParams(); + + Object.entries(updates).forEach(([k, v]) => { + if (v === undefined || (Array.isArray(v) && v.length === 0)) { + return; + } + + if (Array.isArray(v)) { + v.forEach((item) => { + if (item) params.append(k, String(item)); + }); + } else { + params.set(k, String(v)); + } + }); + + const qs = params.toString(); + return `/${locale}/notifications${qs ? `?${qs}` : ''}`; +} diff --git a/app/[locale]/student-activities/clubs/[display_name]/page.tsx b/app/[locale]/student-activities/clubs/[display_name]/page.tsx index 963a9f575..b297e82c1 100644 --- a/app/[locale]/student-activities/clubs/[display_name]/page.tsx +++ b/app/[locale]/student-activities/clubs/[display_name]/page.tsx @@ -14,7 +14,7 @@ import { getS3Url } from '~/server/s3'; import { GalleryCarousel } from '~/components/carousels'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; -import NotificationsPanel from '~/components/notifications-panel'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { Card, CardContent, @@ -368,7 +368,7 @@ export default async function Club({ locale={locale} clubId={club?.id} className="h-[400px]" - viewAllHref={`/${locale}/student-activities/clubs/${display_name}/notifications`} + viewAllHref={`/${locale}/notifications?category=student-activities`} showViewAll={true} /> diff --git a/components/notifications/notification-item-with-modal.tsx b/components/notifications/notification-item-with-modal.tsx new file mode 100644 index 000000000..85c63738f --- /dev/null +++ b/components/notifications/notification-item-with-modal.tsx @@ -0,0 +1,40 @@ +'use client'; + +import { useState } from 'react'; +import { MdOutlineKeyboardArrowRight } from 'react-icons/md'; + +import { NotificationModal } from '~/app/notifications/NotificationModal'; + +interface NotificationItemProps { + id: number; + title: string; + locale: string; +} + +export function NotificationItemWithModal({ + id, + title, + locale, +}: NotificationItemProps) { + const [selectedId, setSelectedId] = useState(null); + + return ( + <> + + + setSelectedId(null)} + locale={locale} + /> + + ); +} diff --git a/components/notifications-panel.tsx b/components/notifications/notifications-panel.tsx similarity index 72% rename from components/notifications-panel.tsx rename to components/notifications/notifications-panel.tsx index 5121fc667..fc31aa1b8 100644 --- a/components/notifications-panel.tsx +++ b/components/notifications/notifications-panel.tsx @@ -1,26 +1,31 @@ import Link from 'next/link'; import { Suspense } from 'react'; -import { MdOutlineKeyboardArrowRight } from 'react-icons/md'; import { Button } from '~/components/buttons'; import Loading from '~/components/loading'; +import { NotificationItemWithModal } from '~/components/notifications/notification-item-with-modal'; import { ScrollArea } from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; import { cn, groupBy } from '~/lib/utils'; -import { - db, - type notifications as notificationsTable, -} from '~/server/db'; +import { db, type notifications as notificationsTable } from '~/server/db'; type NotificationCategory = (typeof notificationsTable.category.enumValues)[number]; +type EducationType = 'ug' | 'pg' | 'phd'; + export interface NotificationsPanelProps { locale: string; /** Filter by notification category */ category?: NotificationCategory; /** Filter by club ID */ clubId?: number; + /** Filter by department ID */ + departmentId?: number; + /** Filter by hostel ID */ + hostelId?: number; + /** Filter by education type (ug, pg, phd) */ + educationType?: EducationType; /** Filter notifications created on or after this date */ startDate?: Date; /** Filter notifications created on or before this date */ @@ -39,6 +44,9 @@ export default async function NotificationsPanel({ locale, category, clubId, + departmentId, + hostelId, + educationType, startDate, endDate, className, @@ -47,7 +55,7 @@ export default async function NotificationsPanel({ viewAllText, }: NotificationsPanelProps) { const text = (await getTranslations(locale)).Notifications; - const filterKey = `${category}-${clubId}-${startDate?.toISOString()}-${endDate?.toISOString()}`; + const filterKey = `${category}-${clubId}-${departmentId}-${hostelId}-${educationType}-${startDate?.toISOString()}-${endDate?.toISOString()}`; return (
    - +
      } key={filterKey}>
    @@ -97,16 +106,24 @@ interface NotificationsListProps { locale: string; category?: NotificationCategory; clubId?: number; + departmentId?: number; + hostelId?: number; + educationType?: EducationType; startDate?: Date; endDate?: Date; + noNotificationsText: string; } const NotificationsList = async ({ locale, category, clubId, + departmentId, + hostelId, + educationType, startDate, endDate, + noNotificationsText, }: NotificationsListProps) => { const notifications = ( await db.query.notifications.findMany({ @@ -119,6 +136,15 @@ const NotificationsList = async ({ if (clubId !== undefined) { conditions.push(eq(notification.clubId, clubId)); } + if (departmentId !== undefined) { + conditions.push(eq(notification.departmentId, departmentId)); + } + if (hostelId !== undefined) { + conditions.push(eq(notification.hostelId, hostelId)); + } + if (educationType) { + conditions.push(eq(notification.educationType, educationType)); + } if (startDate) { conditions.push(gte(notification.createdAt, startDate)); } @@ -140,8 +166,8 @@ const NotificationsList = async ({ if (notifications.length === 0) { return ( -
  • - No notifications found +
  • + {noNotificationsText}
  • ); } @@ -155,15 +181,11 @@ const NotificationsList = async ({
      {notifications.map(({ id, title }, index) => (
    • - - -

      - {title} -

      - +
    • ))}
    diff --git a/components/ui/slider.tsx b/components/ui/slider.tsx index fbc756647..2a89282c5 100644 --- a/components/ui/slider.tsx +++ b/components/ui/slider.tsx @@ -17,10 +17,11 @@ const Slider = React.forwardRef< )} {...props} > - - + + - + + )); Slider.displayName = SliderPrimitive.Root.displayName; diff --git a/i18n/en.ts b/i18n/en.ts index 31e4bd607..dc2ab0398 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -442,14 +442,50 @@ const text: Translations = { signInWithGoogle: 'Sign in with Google', }, Notifications: { - title: 'NOTIFICATIONS', + title: 'Notifications', + searchPlaceholder: 'Search by Title/Content', + clearAll: 'Clear all', + clearAllFilters: 'Clear All Filters', + filterBy: 'Filter By', + noNotificationsFound: 'No notifications found.', + noMoreNotifications: 'No more notifications', + saveSelection: 'Save selection', + viewAll: 'View All', + filter: { + title: 'Filters', + date: 'Date', + category: 'Category', + department: 'Department', + educationType: 'Programme Level', + startDate: 'Start Date', + endDate: 'End Date', + day: 'Day', + month: 'Month', + year: 'Year', + }, categories: { academic: 'Academic', tender: 'Tenders', - workshop: 'Workshops', + workshop: 'Workshops / Seminars', + administration: 'Administration', recruitment: 'Recruitment', + admission: 'Admission', + 'student-activities': 'Student Activities', + faculty: 'Faculty', + research: 'Research & IPR', + alumni: 'Alumni', + examination: 'Examinations', + result: 'Results', + hostel: 'Hostels', + miscellaneous: 'Miscellaneous', + archived: 'Archived', + }, + educationType: { + ug: 'UG', + pg: 'PG', + phd: 'PhD', + all: 'All', }, - viewAll: 'View All', }, Events: { title: 'EVENTS & NEWS', diff --git a/i18n/hi.ts b/i18n/hi.ts index 5cef46703..36c0589ef 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -693,13 +693,49 @@ const text: Translations = { }, Notifications: { title: 'सूचनाएँ', + searchPlaceholder: 'शीर्षक/विषय-वस्तु द्वारा खोजें', + clearAll: 'सभी साफ करें', + clearAllFilters: 'सभी फ़िल्टर साफ करें', + filterBy: 'फ़िल्टर करें', + noNotificationsFound: 'कोई सूचना नहीं मिली।', + noMoreNotifications: 'कोई और सूचनाएँ नहीं', + saveSelection: 'चयन सहेजें', + viewAll: 'सभी देखें', + filter: { + title: 'फ़िल्टर', + date: 'तारीख़', + category: 'श्रेणी', + department: 'विभाग', + educationType: 'कार्यक्रम स्तर', + startDate: 'आरंभ तिथि', + endDate: 'अंतिम तिथि', + day: 'दिन', + month: 'महीना', + year: 'वर्ष', + }, categories: { academic: 'शैक्षणिक', tender: 'निविदाएँ', - workshop: 'कार्यशालाएं', - recruitment: 'नियुक्ति', + workshop: 'कार्यशाला / संगोष्ठी', + administration: 'प्रशासन', + recruitment: 'भर्ती', + admission: 'प्रवेश', + 'student-activities': 'विद्यार्थी गतिविधियाँ', + faculty: 'फैकल्टी', + research: 'अनुसंधान व IPR', + alumni: 'पूर्व छात्र', + examination: 'परीक्षाएँ', + result: 'परिणाम', + hostel: 'हॉस्टल', + miscellaneous: 'अन्य', + archived: 'अभिलेखित', + }, + educationType: { + ug: 'स्नातक', + pg: 'स्नातकोत्तर', + phd: 'पीएचडी', + all: 'सभी', }, - viewAll: 'सारा देखें', }, NotFound: { title: '404', diff --git a/i18n/translations.ts b/i18n/translations.ts index 998bc615c..caff86184 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -419,13 +419,49 @@ export interface Translations { }; Notifications: { title: string; + searchPlaceholder: string; + clearAll: string; + clearAllFilters: string; + filterBy: string; + noNotificationsFound: string; + noMoreNotifications: string; + saveSelection: string; + viewAll: string; + filter: { + title: string; + date: string; + category: string; + department: string; + educationType: string; + startDate: string; + endDate: string; + day: string; + month: string; + year: string; + }; categories: { academic: string; tender: string; workshop: string; + administration: string; recruitment: string; + admission: string; + 'student-activities': string; + faculty: string; + research: string; + alumni: string; + examination: string; + result: string; + hostel: string; + miscellaneous: string; + archived: string; + }; + educationType: { + ug: string; + pg: string; + phd: string; + all: string; }; - viewAll: string; }; Events: { title: string; diff --git a/server/actions/index.ts b/server/actions/index.ts index 7c6eb3ce2..7f401dd34 100644 --- a/server/actions/index.ts +++ b/server/actions/index.ts @@ -1 +1,2 @@ export * from './faculty-profile'; +export * from './notifications'; diff --git a/server/actions/notifications.ts b/server/actions/notifications.ts new file mode 100644 index 000000000..59568f0d5 --- /dev/null +++ b/server/actions/notifications.ts @@ -0,0 +1,157 @@ +'use server'; + +import { and, desc, gte, lt, lte } from 'drizzle-orm'; +import { redirect } from 'next/navigation'; + +import { db } from '~/server/db'; +import { notifications } from '~/server/db/schema'; + +const BATCH_SIZE = 20; + +export interface NotificationItem { + id: number; + title: string; + category: string; + createdAt: string; +} + +export interface LoadMoreResult { + items: NotificationItem[]; + nextCursor: string | null; + hasMore: boolean; +} + +export interface LoadMoreParams { + cursor?: string; + categories?: string[]; + departments?: string[]; + departmentIds?: number[]; + start?: string; + end?: string; + query?: string; +} + +export async function loadMoreNotifications( + params: LoadMoreParams +): Promise { + const { cursor, categories, departmentIds, start, end, query } = params; + + const cursorDate = cursor ? new Date(cursor) : undefined; + const startDate = start ? new Date(start) : undefined; + const endDate = end ? new Date(end) : undefined; + + // Build conditions + const conditions = []; + if (startDate) conditions.push(gte(notifications.createdAt, startDate)); + if (endDate) conditions.push(lte(notifications.createdAt, endDate)); + if (cursorDate) conditions.push(lt(notifications.createdAt, cursorDate)); + + // Fetch batch + 1 to check if there are more + let results = await db.query.notifications.findMany({ + where: conditions.length ? and(...conditions) : undefined, + orderBy: [desc(notifications.createdAt)], + limit: BATCH_SIZE + 1, + }); + + // Apply in-memory filters (category, department, text search) + if (categories?.length) { + results = results.filter((n) => categories.includes(n.category)); + } + + if (departmentIds?.length) { + results = results.filter( + (n) => n.departmentId && departmentIds.includes(n.departmentId) + ); + } + + if (query) { + const lowerQuery = query.toLowerCase(); + results = results.filter( + (n) => + n.title.toLowerCase().includes(lowerQuery) || + n.content?.toLowerCase().includes(lowerQuery) + ); + } + + const hasMore = results.length > BATCH_SIZE; + const items = hasMore ? results.slice(0, BATCH_SIZE) : results; + const nextCursor = hasMore + ? items[items.length - 1]?.createdAt.toISOString() + : null; + + // Serialize for client + const serializedItems: NotificationItem[] = items.map((n) => ({ + id: n.id, + title: n.title, + category: n.category, + createdAt: n.createdAt.toISOString(), + })); + + return { + items: serializedItems, + nextCursor, + hasMore, + }; +} + +// Full notification details for modal +export interface NotificationDetails { + id: number; + title: string; + content: string | null; + category: string; + documents: string[]; + createdAt: string; +} + +export async function getNotificationById( + id: number +): Promise { + const notification = await db.query.notifications.findFirst({ + where: (n, { eq }) => eq(n.id, id), + }); + + if (!notification) return null; + + return { + id: notification.id, + title: notification.title, + content: notification.content, + category: notification.category, + documents: notification.documents, + createdAt: notification.createdAt.toISOString(), + }; +} + +export async function applyDateFilter(formData: FormData) { + const locale = formData.get('locale')?.toString() ?? 'en'; + const startDay = formData.get('start-day')?.toString(); + const startMonth = formData.get('start-month')?.toString(); + const startYear = formData.get('start-year')?.toString(); + const endDay = formData.get('end-day')?.toString(); + const endMonth = formData.get('end-month')?.toString(); + const endYear = formData.get('end-year')?.toString(); + + const categories = formData.getAll('category') as string[]; + const departments = formData.getAll('department') as string[]; + const q = formData.get('q')?.toString(); + + const startVal = + startYear && startMonth && startDay + ? `${startYear}-${startMonth.padStart(2, '0')}-${startDay.padStart(2, '0')}` + : undefined; + const endVal = + endYear && endMonth && endDay + ? `${endYear}-${endMonth.padStart(2, '0')}-${endDay.padStart(2, '0')}` + : undefined; + + const params = new URLSearchParams(); + if (startVal) params.set('start', startVal); + if (endVal) params.set('end', endVal); + if (q) params.set('q', q); + categories.forEach((c) => params.append('category', c)); + departments.forEach((d) => params.append('department', d)); + + const qs = params.toString(); + redirect(`/${locale}/notifications${qs ? `?${qs}` : ''}`); +} diff --git a/server/db/schema/notifications.schema.ts b/server/db/schema/notifications.schema.ts index f5a017795..fe8f7a714 100644 --- a/server/db/schema/notifications.schema.ts +++ b/server/db/schema/notifications.schema.ts @@ -1,45 +1,90 @@ -import { check, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; +import { check, pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; import { clubs } from './clubs.schema'; +import { departments } from './departments.schema'; +import { hostels } from './hostels.schema'; + +export const notificationCategoryEnum = pgEnum('notification_category', [ + 'academic', + 'tender', + 'workshop', + 'administration', + 'recruitment', + 'admission', + 'student-activities', + 'faculty', + 'research', + 'alumni', + 'examination', + 'result', + 'hostel', + 'miscellaneous', + 'archived', + 'placements', +]); export const notifications = pgTable( 'notifications', (t) => ({ - id: t.serial().primaryKey(), - title: t.varchar({ length: 256 }).unique().notNull(), - content: t.text(), - category: t - .varchar({ - enum: [ - 'academic', - 'tender', - 'workshop', - 'recruitment', - 'student-activity', - 'hostel', - ], - }) - .notNull(), - createdAt: t.timestamp().defaultNow().notNull(), + id: t.serial('id').primaryKey(), + title: t.varchar('title', { length: 256 }).unique().notNull(), + content: t.text('content'), + + category: notificationCategoryEnum('category').notNull(), + + educationType: t.varchar('education_type', { + enum: ['ug', 'pg', 'phd'], + }), + documents: t + .text('documents') + .array() + .notNull() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), updatedAt: t - .timestamp() + .timestamp('updated_at') .$onUpdate(() => new Date()) .notNull(), - clubId: t.integer().references(() => clubs.id), + clubId: t.integer('club_id').references(() => clubs.id), + departmentId: t.integer('department_id').references(() => departments.id), + hostelId: t.integer('hostel_id').references(() => hostels.id), }), - (notifications) => { - return { - notificationsTitleIndex: uniqueIndex('notifications_title_idx').on( - notifications.title - ), - // Add check constraint - clubrequiredforStudentActivity: check( - 'clubIdRequiredForStudentActivity', - sql`${notifications.category} != 'student-activity' OR ${notifications.clubId} IS NOT NULL` - ), - }; - } + (n) => ({ + notificationsTitleIndex: uniqueIndex('notifications_title_idx').on(n.title), + clubRequiredForStudent: check( + 'club_required_for_student', + sql`( + (${n.category} = 'student-activities') + OR + (${n.category} != 'student-activities' AND ${n.clubId} IS NULL) + )` + ), + educationTypeRequiredForAcademicAdmission: check( + 'education_type_required_for_academic_admission', + sql`( + (${n.category} IN ('academic','admission')) + OR + (${n.category} NOT IN ('academic','admission') AND ${n.educationType} IS NULL) + )` + ), + hostelRequiredForHostel: check( + 'hostel_required_for_hostel', + sql`( + (${n.category} = 'hostel') + OR + (${n.category} != 'hostel' AND ${n.hostelId} IS NULL) + )` + ), + departmentAllowedOnlyWhenRelevant: check( + 'department_allowed_only_when_relevant', + sql`( + (${n.category} IN ('academic','workshop','administration','recruitment','admission','faculty','research','examination','result')) + OR + (${n.departmentId} IS NULL) + )` + ), + }) ); export const notificationsRelations = relations(notifications, ({ one }) => ({ @@ -47,4 +92,12 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({ fields: [notifications.clubId], references: [clubs.id], }), + department: one(departments, { + fields: [notifications.departmentId], + references: [departments.id], + }), + hostel: one(hostels, { + fields: [notifications.hostelId], + references: [hostels.id], + }), })); From 1dec6ad2f4032952be5fe47009dc5a6979784d4c Mon Sep 17 00:00:00 2001 From: Aryawart-kathpal Date: Sat, 17 Jan 2026 02:05:10 +0530 Subject: [PATCH 15/73] fix: Notifications categories to enum array --- app/[locale]/notifications/DateRangeForm.tsx | 24 +++++++++++++++---- .../notifications/NotificationModal.tsx | 2 +- app/[locale]/notifications/page.tsx | 15 +++++++----- .../notifications/notifications-panel.tsx | 11 +++++---- server/actions/notifications.ts | 12 ++++++---- server/db/schema/notifications.schema.ts | 20 +++++++++------- 6 files changed, 55 insertions(+), 29 deletions(-) diff --git a/app/[locale]/notifications/DateRangeForm.tsx b/app/[locale]/notifications/DateRangeForm.tsx index 5e93d7438..195513fd5 100644 --- a/app/[locale]/notifications/DateRangeForm.tsx +++ b/app/[locale]/notifications/DateRangeForm.tsx @@ -33,9 +33,12 @@ export function DateRangeForm({ const pathname = usePathname(); const searchParams = useSearchParams(); + const currentYear = new Date().getFullYear(); + const today = new Date(); + const [yearRange, setYearRange] = React.useState([ start ? new Date(start).getFullYear() : 2000, - end ? new Date(end).getFullYear() : 2025, + end ? new Date(end).getFullYear() : currentYear, ]); const startDate = start ? new Date(start) : undefined; @@ -45,9 +48,11 @@ export function DateRangeForm({ const [startMonth, setStartMonth] = React.useState( startDate ? startDate.getMonth() + 1 : 1 ); - const [endDay, setEndDay] = React.useState(endDate?.getDate() ?? 1); + const [endDay, setEndDay] = React.useState( + endDate?.getDate() ?? today.getDate() + ); const [endMonth, setEndMonth] = React.useState( - endDate ? endDate.getMonth() + 1 : 1 + endDate ? endDate.getMonth() + 1 : today.getMonth() + 1 ); // Apply filters to URL - only called on explicit user actions @@ -80,7 +85,16 @@ export function DateRangeForm({ const newUrl = `${pathname}?${params.toString()}`; router.push(newUrl, { scroll: false }); }, - [yearRange, startDay, startMonth, endDay, endMonth, searchParams, pathname, router] + [ + yearRange, + startDay, + startMonth, + endDay, + endMonth, + searchParams, + pathname, + router, + ] ); return ( @@ -97,7 +111,7 @@ export function DateRangeForm({ 0 && ( -
    +
    2 ? 'h-32 sm:h-auto' : '' diff --git a/app/[locale]/notifications/page.tsx b/app/[locale]/notifications/page.tsx index a0f79cfed..637e07f8f 100644 --- a/app/[locale]/notifications/page.tsx +++ b/app/[locale]/notifications/page.tsx @@ -9,6 +9,7 @@ import ImageHeader from '~/components/image-header'; import { Button } from '~/components/buttons'; import { ScrollArea } from '~/components/ui'; import { notifications as notificationsSchema } from '~/server/db'; +import { notificationCategoryEnum } from '~/server/db/schema/notifications.schema'; import { type NotificationItem } from '~/server/actions/notifications'; import { DateRangeForm } from './DateRangeForm'; @@ -17,7 +18,7 @@ import { MultiCheckbox } from './MultiCheckbox'; import { NotificationsList } from './NotificationsList'; import { SearchInput } from './SearchInput'; -type Cat = (typeof notificationsSchema.category.enumValues)[number]; +type Cat = (typeof notificationCategoryEnum.enumValues)[number]; const INITIAL_BATCH_SIZE = 20; @@ -68,9 +69,11 @@ export default async function NotificationsPage({ limit: INITIAL_BATCH_SIZE + 1, // +1 to check if there are more }); - // Category filter (multi) + // Category filter (multi) - check if any of notification's categories match selected if (categories.length) { - raw = raw.filter((n) => categories.includes(n.category as Cat)); + raw = raw.filter((n) => + n.categories.some((cat) => categories.includes(cat as Cat)) + ); } // Department filter (multi via foreign key, if departmentId present) @@ -98,7 +101,7 @@ export default async function NotificationsPage({ const serializedItems: NotificationItem[] = initialItems.map((n) => ({ id: n.id, title: n.title, - category: n.category, + categories: n.categories, createdAt: n.createdAt.toISOString(), })); @@ -169,7 +172,7 @@ export default async function NotificationsPage({ categories.includes(n.category)); + results = results.filter((n) => + n.categories.some((cat) => categories.includes(cat)) + ); } if (departmentIds?.length) { @@ -83,7 +85,7 @@ export async function loadMoreNotifications( const serializedItems: NotificationItem[] = items.map((n) => ({ id: n.id, title: n.title, - category: n.category, + categories: n.categories, createdAt: n.createdAt.toISOString(), })); @@ -99,7 +101,7 @@ export interface NotificationDetails { id: number; title: string; content: string | null; - category: string; + categories: string[]; documents: string[]; createdAt: string; } @@ -117,7 +119,7 @@ export async function getNotificationById( id: notification.id, title: notification.title, content: notification.content, - category: notification.category, + categories: notification.categories, documents: notification.documents, createdAt: notification.createdAt.toISOString(), }; diff --git a/server/db/schema/notifications.schema.ts b/server/db/schema/notifications.schema.ts index fe8f7a714..4d74f797c 100644 --- a/server/db/schema/notifications.schema.ts +++ b/server/db/schema/notifications.schema.ts @@ -31,7 +31,11 @@ export const notifications = pgTable( title: t.varchar('title', { length: 256 }).unique().notNull(), content: t.text('content'), - category: notificationCategoryEnum('category').notNull(), + // NEW - array of categories + categories: notificationCategoryEnum('categories') + .array() + .notNull() + .default(sql`'{}'::notification_category[]`), educationType: t.varchar('education_type', { enum: ['ug', 'pg', 'phd'], @@ -55,31 +59,31 @@ export const notifications = pgTable( clubRequiredForStudent: check( 'club_required_for_student', sql`( - (${n.category} = 'student-activities') + ('student-activities' = ANY(${n.categories})) OR - (${n.category} != 'student-activities' AND ${n.clubId} IS NULL) + (NOT ('student-activities' = ANY(${n.categories})) AND ${n.clubId} IS NULL) )` ), educationTypeRequiredForAcademicAdmission: check( 'education_type_required_for_academic_admission', sql`( - (${n.category} IN ('academic','admission')) + ('academic' = ANY(${n.categories}) OR 'admission' = ANY(${n.categories})) OR - (${n.category} NOT IN ('academic','admission') AND ${n.educationType} IS NULL) + (NOT ('academic' = ANY(${n.categories})) AND NOT ('admission' = ANY(${n.categories})) AND ${n.educationType} IS NULL) )` ), hostelRequiredForHostel: check( 'hostel_required_for_hostel', sql`( - (${n.category} = 'hostel') + ('hostel' = ANY(${n.categories})) OR - (${n.category} != 'hostel' AND ${n.hostelId} IS NULL) + (NOT ('hostel' = ANY(${n.categories})) AND ${n.hostelId} IS NULL) )` ), departmentAllowedOnlyWhenRelevant: check( 'department_allowed_only_when_relevant', sql`( - (${n.category} IN ('academic','workshop','administration','recruitment','admission','faculty','research','examination','result')) + ('academic' = ANY(${n.categories}) OR 'workshop' = ANY(${n.categories}) OR 'administration' = ANY(${n.categories}) OR 'recruitment' = ANY(${n.categories}) OR 'admission' = ANY(${n.categories}) OR 'faculty' = ANY(${n.categories}) OR 'research' = ANY(${n.categories}) OR 'examination' = ANY(${n.categories}) OR 'result' = ANY(${n.categories})) OR (${n.departmentId} IS NULL) )` From a4155d046d15f01000805a2d700f6e704d7994da Mon Sep 17 00:00:00 2001 From: Aryawart Kathpal <132134276+Aryawart-kathpal@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:02:37 +0530 Subject: [PATCH 16/73] Events and News Page and Modal Co-authored-by: Swastik Bhowmick --- app/[locale]/EventsGrid.tsx | 84 ++++++++ app/[locale]/events.tsx | 97 ++++----- app/[locale]/events/DateRangeForm.tsx | 204 +++++++++++++++++++ app/[locale]/events/EventModal.tsx | 157 +++++++++++++++ app/[locale]/events/EventsList.tsx | 211 ++++++++++++++++++++ app/[locale]/events/MobileFilters.tsx | 221 +++++++++++++++++++++ app/[locale]/events/MultiCheckbox.tsx | 194 ++++++++++++++++++ app/[locale]/events/SearchInput.tsx | 48 +++++ app/[locale]/events/page.tsx | 274 +++++++++++++++++++++++++- app/[locale]/page.tsx | 7 +- components/ui/dialog.tsx | 2 +- i18n/en.ts | 27 ++- i18n/hi.ts | 27 ++- i18n/translations.ts | 27 ++- server/actions/events.ts | 81 ++++++++ server/actions/index.ts | 1 + server/db/schema/events.schema.ts | 52 +++-- server/db/schema/faculty.schema.ts | 2 +- 18 files changed, 1631 insertions(+), 85 deletions(-) create mode 100644 app/[locale]/EventsGrid.tsx create mode 100644 app/[locale]/events/DateRangeForm.tsx create mode 100644 app/[locale]/events/EventModal.tsx create mode 100644 app/[locale]/events/EventsList.tsx create mode 100644 app/[locale]/events/MobileFilters.tsx create mode 100644 app/[locale]/events/MultiCheckbox.tsx create mode 100644 app/[locale]/events/SearchInput.tsx create mode 100644 server/actions/events.ts diff --git a/app/[locale]/EventsGrid.tsx b/app/[locale]/EventsGrid.tsx new file mode 100644 index 000000000..1ca61edcf --- /dev/null +++ b/app/[locale]/EventsGrid.tsx @@ -0,0 +1,84 @@ +'use client'; + +import Image from 'next/image'; +import { useState } from 'react'; + +import { Card, CardContent, CardDescription, CardTitle } from '~/components/ui'; +import { cn } from '~/lib/utils'; + +import { EventModal } from './events/EventModal'; +import type { EventItem } from './events/EventsList'; + +interface EventsGridProps { + events: EventItem[]; + locale: string; + s3Url: string; +} + +export function EventsGrid({ events, locale, s3Url }: EventsGridProps) { + const [selectedEvent, setSelectedEvent] = useState(null); + + return ( + <> +
      + {events.map((event, index) => ( +
    1. setSelectedEvent(event)} + > + + {event.title} 0 + ? event.images[0] + : `${s3Url}/events/${event.startDate.slice(0, 4)}/${event.startDate.slice(5, 7)}/${event.title}/image01.jpg` + } + width={0} + /> + + + + {event.title} + + + {event.description} + + + +
    2. + ))} +
    + + setSelectedEvent(null)} + locale={locale} + /> + + ); +} diff --git a/app/[locale]/events.tsx b/app/[locale]/events.tsx index 44ec81c9a..30d8a9d08 100644 --- a/app/[locale]/events.tsx +++ b/app/[locale]/events.tsx @@ -1,4 +1,4 @@ -import { desc, eq } from 'drizzle-orm'; +import { arrayOverlaps, desc, eq } from 'drizzle-orm'; import Image from 'next/image'; import Link from 'next/link'; @@ -11,30 +11,40 @@ import { SelectTrigger, SelectValue, } from '~/components/inputs'; -import { Card, CardContent, CardDescription, CardTitle } from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; -import { cn, getKeys } from '~/lib/utils'; -import { db, type events as eventsSchema } from '~/server/db'; +import { cn } from '~/lib/utils'; +import { db, type eventCategoryEnum } from '~/server/db'; import { getS3Url } from '~/server/s3'; +import { EventsGrid } from './EventsGrid'; + +type Cat = (typeof eventCategoryEnum.enumValues)[number]; + +// Categories to display on the landing page (featured is special - not a real category) +const DISPLAY_CATEGORIES: (Cat | 'featured')[] = [ + 'featured', + 'cultural', + 'technical', + 'campus-highlights', + 'academic', +]; + export default async function Events({ category: currentCategory, locale, }: { - category: - | (typeof eventsSchema.category.enumValues)[number] - | 'recents' - | 'featured'; + category: Cat | 'featured'; locale: string; }) { const text = (await getTranslations(locale)).Events; const events = await db.query.events.findMany({ where: (event) => { - if (currentCategory === 'recents') return undefined; - else if (currentCategory === 'featured') + if (currentCategory === 'featured') { return eq(event.isFeatured, true); - return eq(event.category, currentCategory); + } + // Check if the event's categories array contains the selected category + return arrayOverlaps(event.categories, [currentCategory]); }, orderBy: (event) => [desc(event.startDate)], limit: 6, @@ -71,7 +81,7 @@ export default async function Events({ 'h-fit min-w-72 space-y-4' )} > - {getKeys(text.categories).map((category, index) => ( + {DISPLAY_CATEGORIES.map((category, index) => (
  • - {getKeys(text.categories).map((category, index) => ( + {DISPLAY_CATEGORIES.map((category, index) => ( {text.categories[category]} @@ -128,50 +138,23 @@ export default async function Events({ linkProps={{ href: `/${locale}/events` }} text={text.viewAll} /> -
      - {events.map(({ title, description, startDate }, index) => ( -
    1. - - {title} - - - {title} - - {description} - - - -
    2. - ))} -
    + ({ + id: e.id, + title: e.title, + description: e.description, + categories: e.categories, + startDate: e.startDate, + endDate: e.endDate, + time: e.time, + location: e.location, + locationUrl: e.locationUrl, + images: e.images, + documents: e.documents, + }))} + locale={locale} + s3Url={getS3Url()} + />
  • diff --git a/app/[locale]/events/DateRangeForm.tsx b/app/[locale]/events/DateRangeForm.tsx new file mode 100644 index 000000000..b8dcc7000 --- /dev/null +++ b/app/[locale]/events/DateRangeForm.tsx @@ -0,0 +1,204 @@ +'use client'; + +import React from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; + +import { cn } from '~/lib/utils'; +import { Slider } from '~/components/ui'; + +interface DateRangeFormText { + startDate: string; + endDate: string; + day: string; + month: string; + year: string; +} + +export function DateRangeForm({ + start, + end, + compact = false, + text, +}: { + locale: string; + categories?: string[]; + query?: string; + start?: string; + end?: string; + compact?: boolean; + text?: DateRangeFormText; +}) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + + const currentDate = new Date(); + const currentYear = currentDate.getFullYear(); + + const [yearRange, setYearRange] = React.useState([ + start ? new Date(start).getFullYear() : 2000, + end ? new Date(end).getFullYear() : currentYear, + ]); + + const startDate = start ? new Date(start) : undefined; + const endDate = end ? new Date(end) : undefined; + + const [startDay, setStartDay] = React.useState(startDate?.getDate() ?? 1); + const [startMonth, setStartMonth] = React.useState( + startDate ? startDate.getMonth() + 1 : 1 + ); + const [endDay, setEndDay] = React.useState( + endDate?.getDate() ?? currentDate.getDate() + ); + const [endMonth, setEndMonth] = React.useState( + endDate ? endDate.getMonth() + 1 : currentDate.getMonth() + 1 + ); + + // Apply filters to URL - only called on explicit user actions + const applyFilters = React.useCallback( + (newYearRange?: number[]) => { + const params = new URLSearchParams(searchParams); + const years = newYearRange ?? yearRange; + + const startVal = + years[0] && startMonth && startDay + ? `${years[0]}-${String(startMonth).padStart(2, '0')}-${String(startDay).padStart(2, '0')}` + : undefined; + const endVal = + years[1] && endMonth && endDay + ? `${years[1]}-${String(endMonth).padStart(2, '0')}-${String(endDay).padStart(2, '0')}` + : undefined; + + if (startVal) { + params.set('start', startVal); + } else { + params.delete('start'); + } + + if (endVal) { + params.set('end', endVal); + } else { + params.delete('end'); + } + + const newUrl = `${pathname}?${params.toString()}`; + router.push(newUrl, { scroll: false }); + }, + [ + yearRange, + startDay, + startMonth, + endDay, + endMonth, + searchParams, + pathname, + router, + ] + ); + + return ( +
    + {/* Year Range Slider */} +
    +
    +
    + {yearRange[0]} +
    +
    + {yearRange[1]} +
    +
    + { + setYearRange(value); + applyFilters(value); + }} + className="w-full" + /> +
    + + {/* Start Date */} +
    + +
    + setStartDay(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setStartMonth(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setYearRange([+e.target.value, yearRange[1]])} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> +
    +
    + + {/* End Date */} +
    + +
    + setEndDay(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setEndMonth(+e.target.value)} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> + setYearRange([yearRange[0], +e.target.value])} + onBlur={() => applyFilters()} + className="bg-white rounded border border-neutral-300 px-2 py-2 text-sm placeholder:text-neutral-400" + /> +
    +
    +
    + ); +} diff --git a/app/[locale]/events/EventModal.tsx b/app/[locale]/events/EventModal.tsx new file mode 100644 index 000000000..a3a9ecca2 --- /dev/null +++ b/app/[locale]/events/EventModal.tsx @@ -0,0 +1,157 @@ +'use client'; + +import Image from 'next/image'; +import { + MdAccessTime, + MdCalendarToday, + MdLocationOn, + MdOpenInNew, +} from 'react-icons/md'; + +import { GalleryCarousel } from '~/components/carousels'; +import { Dialog, DialogContent, ScrollArea } from '~/components/ui'; + +import type { EventItem } from './EventsList'; + +interface EventModalProps { + event: EventItem | null; + onClose: () => void; + locale: string; +} + +export function EventModal({ event, onClose, locale }: EventModalProps) { + const formatDate = (startDate: string, endDate: string | null) => { + const start = new Date(startDate); + const options: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'long', + year: 'numeric', + }; + + if (!endDate) { + return start.toLocaleDateString(locale, options); + } + + const end = new Date(endDate); + return `${start.toLocaleDateString(locale, options)} - ${end.toLocaleDateString(locale, options)}`; + }; + + // Extract filename from URL for document button label (without extension) + const getDocumentName = (url: string, index: number) => { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop(); + if (filename) { + // Decode URI and remove extension + const decoded = decodeURIComponent(filename); + const nameWithoutExt = decoded.replace(/\.[^/.]+$/, ''); + return nameWithoutExt || decoded; + } + } catch { + // If URL parsing fails, use generic name + } + return `Document ${index + 1}`; + }; + + return ( + !open && onClose()}> + + {event && ( +
    + {/* Title */} +

    + {event.title} +

    + + {/* Image Carousel */} + {event.images.length > 0 && ( +
    + + {event.images.map((img, index) => ( +
    + {`Image +
    + ))} +
    +
    + )} + + {/* Description - Scrollable */} + {event.description && ( + +

    + {event.description} +

    +
    + )} + + {/* Documents - Scrollable */} + {event.documents && event.documents.length > 0 && ( + +
    + {event.documents.map((doc, index) => ( + + + {getDocumentName(doc, index)} + + + + ))} +
    +
    + )} + + {/* Date & Location & Time */} +
    + + + {formatDate(event.startDate, event.endDate)} + + + {event.location && ( + + + {event.locationUrl ? ( + + {event.location} + + ) : ( + event.location + )} + + )} + + {event.time && ( + + + {event.time} + + )} +
    +
    + )} +
    +
    + ); +} diff --git a/app/[locale]/events/EventsList.tsx b/app/[locale]/events/EventsList.tsx new file mode 100644 index 000000000..d265af184 --- /dev/null +++ b/app/[locale]/events/EventsList.tsx @@ -0,0 +1,211 @@ +'use client'; + +import Image from 'next/image'; +import { useCallback, useEffect, useRef, useState } from 'react'; +import { MdCalendarToday, MdLocationOn } from 'react-icons/md'; + +import { NoResultStatus } from '~/components/status'; +import { loadMoreEvents } from '~/server/actions'; + +import { EventModal } from './EventModal'; + +export interface EventItem { + id: number; + title: string; + description: string | null; + categories: string[]; + startDate: string; + endDate: string | null; + time: string | null; + location: string | null; + locationUrl: string | null; + images: string[]; + documents: string[]; +} + +interface FilterParams { + categories?: string[]; + start?: string; + end?: string; + query?: string; +} + +interface EventsListProps { + initialItems: EventItem[]; + initialCursor: string | null; + initialHasMore: boolean; + locale: string; + filterParams: FilterParams; + text: { + noEventsFound: string; + noMoreEvents: string; + }; +} + +export function EventsList({ + initialItems, + initialCursor, + initialHasMore, + locale, + filterParams, + text, +}: EventsListProps) { + const [items, setItems] = useState(initialItems); + const [cursor, setCursor] = useState(initialCursor); + const [hasMore, setHasMore] = useState(initialHasMore); + const [isLoading, setIsLoading] = useState(false); + const [selectedEvent, setSelectedEvent] = useState(null); + const loaderRef = useRef(null); + + // Reset when filter params change + useEffect(() => { + setItems(initialItems); + setCursor(initialCursor); + setHasMore(initialHasMore); + }, [initialItems, initialCursor, initialHasMore]); + + const loadMore = useCallback(async () => { + if (isLoading || !hasMore || !cursor) return; + + setIsLoading(true); + try { + const data = await loadMoreEvents({ + cursor, + categories: filterParams.categories, + start: filterParams.start, + end: filterParams.end, + query: filterParams.query, + }); + + setItems((prev) => [...prev, ...data.items]); + setCursor(data.cursor); + setHasMore(data.hasMore); + } catch (error) { + console.error('Failed to load more events:', error); + } finally { + setIsLoading(false); + } + }, [isLoading, hasMore, cursor, filterParams]); + + // Infinite scroll observer + useEffect(() => { + const loader = loaderRef.current; + if (!loader) return; + + const observer = new IntersectionObserver( + (entries) => { + if (entries[0].isIntersecting && hasMore && !isLoading) { + void loadMore(); + } + }, + { threshold: 0.1 } + ); + + observer.observe(loader); + return () => observer.disconnect(); + }, [loadMore, hasMore, isLoading]); + + if (items.length === 0) { + return ; + } + + const formatDate = (startDate: string, endDate: string | null) => { + const start = new Date(startDate); + const options: Intl.DateTimeFormatOptions = { + day: 'numeric', + month: 'long', + year: 'numeric', + }; + + if (!endDate) { + return start.toLocaleDateString(locale, options); + } + + const end = new Date(endDate); + return `${start.toLocaleDateString(locale, options)} - ${end.toLocaleDateString(locale, options)}`; + }; + + return ( + <> +
      + {items.map((event) => ( +
    1. setSelectedEvent(event)} + > +
      +
      +

      + {event.title} +

      + {event.description && ( +

      + {event.description} +

      + )} + +
      + + + {formatDate(event.startDate, event.endDate)} + + {event.location && ( + + + {event.locationUrl ? ( + e.stopPropagation()} + > + {event.location} + + ) : ( + event.location + )} + + )} +
      +
      + + {event.images.length > 0 && ( +
      + {event.title} +
      + )} +
      +
    2. + ))} +
    + + {/* Loader / End message */} +
    + {isLoading && ( +
    +
    +
    + )} + {!hasMore && items.length > 0 && ( +

    + {text.noMoreEvents} +

    + )} +
    + + {/* Event Modal */} + setSelectedEvent(null)} + locale={locale} + /> + + ); +} diff --git a/app/[locale]/events/MobileFilters.tsx b/app/[locale]/events/MobileFilters.tsx new file mode 100644 index 000000000..91078ad65 --- /dev/null +++ b/app/[locale]/events/MobileFilters.tsx @@ -0,0 +1,221 @@ +'use client'; + +import { createPortal } from 'react-dom'; +import React, { useEffect, useRef, useState } from 'react'; +import Link from 'next/link'; +import { FaTimes } from 'react-icons/fa'; +import { MdFilterList } from 'react-icons/md'; + +import { ScrollArea } from '~/components/ui/scroll-area'; +import { cn } from '~/lib/utils'; + +import { DateRangeForm } from './DateRangeForm'; +import { MultiCheckbox } from './MultiCheckbox'; + +type Cat = string; + +interface MobileFiltersProps { + locale: string; + categories: Cat[]; + categoryOptions: readonly string[]; + query: string; + start?: string; + end?: string; + text: { + filters: string; + filterBy: string; + clearAllFilters: string; + filter: { + date: string; + category: string; + startDate: string; + endDate: string; + day: string; + month: string; + year: string; + }; + categories: Record; + }; + className?: string; +} + +export function MobileFilters({ + locale, + categories, + categoryOptions, + query, + start, + end, + text, + className, +}: MobileFiltersProps) { + const [open, setOpen] = useState(false); + const [isAnimating, setIsAnimating] = useState(false); + const panelRef = useRef(null); + + // Handle close with animation + const handleClose = () => { + setIsAnimating(false); + setTimeout(() => setOpen(false), 300); + }; + + // Trigger animation after open + useEffect(() => { + if (open) { + requestAnimationFrame(() => setIsAnimating(true)); + } + }, [open]); + + // Lock body scroll while open + useEffect(() => { + const prev = document.body.style.overflow; + if (open) document.body.style.overflow = 'hidden'; + return () => { + document.body.style.overflow = prev; + }; + }, [open]); + + // Close on outside click + useEffect(() => { + if (!open) return; + + document.body.classList.add('overflow-hidden'); + return () => { + document.body.classList.remove('overflow-hidden'); + }; + }, [open]); + + // Calculate active filters count (including date filters) + const dateFiltersCount = (start ? 1 : 0) + (end ? 1 : 0); + const activeFiltersCount = categories.length + dateFiltersCount; + + return ( +
    + {/* Filter button */} + + + {/* Backdrop */} +
    +
    +
    + + {open && + createPortal( + , + document.body + )} +
    + ); +} diff --git a/app/[locale]/events/MultiCheckbox.tsx b/app/[locale]/events/MultiCheckbox.tsx new file mode 100644 index 000000000..258636bbc --- /dev/null +++ b/app/[locale]/events/MultiCheckbox.tsx @@ -0,0 +1,194 @@ +'use client'; + +import Link from 'next/link'; +import { useSearchParams } from 'next/navigation'; + +import { cn } from '~/lib/utils'; +import { ScrollArea } from '~/components/ui'; +import { + Select, + SelectContent, + SelectTrigger, + SelectValue, +} from '~/components/inputs'; + +export function MultiCheckbox({ + param, + options, + selected, + locale, + textMap, + select = false, +}: { + param: string; + options: readonly string[]; + selected: string[]; + locale: string; + textMap: Record; + select?: boolean; +}) { + const searchParams = useSearchParams(); + + const isAllSelected = selected.length === 0; + + // Sort options: selected ones first, then unselected + const sortedOptions = [...options].sort((a, b) => { + const aSelected = selected.includes(a); + const bSelected = selected.includes(b); + if (aSelected && !bSelected) return -1; + if (!aSelected && bSelected) return 1; + return 0; + }); + + const getUpdatedValues = (option: string) => { + return selected.includes(option) + ? selected.filter((s) => s !== option) + : [...selected, option]; + }; + + const buildLocalHref = (updates: Record) => { + const params = new URLSearchParams(searchParams); + + Object.entries(updates).forEach(([k, v]) => { + if (v === undefined || (Array.isArray(v) && v.length === 0)) { + params.delete(k); + return; + } + + if (Array.isArray(v)) { + params.delete(k); + v.forEach((item) => { + if (item) params.append(k, String(item)); + }); + } else { + params.set(k, String(v)); + } + }); + + const qs = params.toString(); + return `/${locale}/events${qs ? `?${qs}` : ''}`; + }; + + return select ? ( + + + All + +
    +
    + {sortedOptions.map((opt) => ( +
    + + + {textMap[opt] ?? opt} + +
    + ))} + + + ) : ( + +
      + {/* All Option */} +
    1. + +
      +
      + +
      + All +
      + +
    2. + {sortedOptions.map((opt) => { + const isChecked = selected.includes(opt); + return ( +
    3. + +
      +
      + +
      + + {textMap[opt] ?? opt} + +
      + +
    4. + ); + })} +
    +
    + ); +} diff --git a/app/[locale]/events/SearchInput.tsx b/app/[locale]/events/SearchInput.tsx new file mode 100644 index 000000000..060900e93 --- /dev/null +++ b/app/[locale]/events/SearchInput.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { usePathname, useRouter, useSearchParams } from 'next/navigation'; +import { MdSearch } from 'react-icons/md'; + +export function SearchInput({ + defaultValue, + placeholder, +}: { + defaultValue?: string; + placeholder: string; +}) { + const router = useRouter(); + const pathname = usePathname(); + const searchParams = useSearchParams(); + const [query, setQuery] = useState(defaultValue ?? ''); + + useEffect(() => { + const timer = setTimeout(() => { + const params = new URLSearchParams(searchParams); + + if (query.trim()) { + params.set('q', query.trim()); + } else { + params.delete('q'); + } + + router.push(`${pathname}?${params.toString()}`, { scroll: false }); + }, 300); + + return () => clearTimeout(timer); + }, [query, searchParams, pathname, router]); + + return ( +
    + + setQuery(e.target.value)} + placeholder={placeholder} + className="bg-white w-full rounded-md border border-neutral-300 px-3 py-2 pl-10 text-sm placeholder:text-neutral-400 focus:border-primary-700 focus:outline-none focus:ring-1 focus:ring-primary-700" + /> +
    + ); +} diff --git a/app/[locale]/events/page.tsx b/app/[locale]/events/page.tsx index b21a36cbb..14cff99dc 100644 --- a/app/[locale]/events/page.tsx +++ b/app/[locale]/events/page.tsx @@ -1,9 +1,277 @@ -import { WorkInProgressStatus } from '~/components/status'; +import Link from 'next/link'; +import React, { Suspense } from 'react'; +import { desc } from 'drizzle-orm'; -export default function Events({ +import { getTranslations } from '~/i18n/translations'; +import { db, eventCategoryEnum } from '~/server/db'; +import { cn } from '~/lib/utils'; +import ImageHeader from '~/components/image-header'; +import { Button } from '~/components/buttons'; +import { ScrollArea } from '~/components/ui'; +import Loading from '~/components/loading'; + +import { DateRangeForm } from './DateRangeForm'; +import { MobileFilters } from './MobileFilters'; +import { MultiCheckbox } from './MultiCheckbox'; +import { type EventItem, EventsList } from './EventsList'; +import { SearchInput } from './SearchInput'; + +type Cat = (typeof eventCategoryEnum.enumValues)[number]; +const INITIAL_BATCH_SIZE = 20; + +interface PageSearchParams { + q?: string; + category?: string | string[]; + start?: string; + end?: string; +} + +export default async function EventsPage({ params: { locale }, + searchParams, }: { params: { locale: string }; + searchParams: PageSearchParams; +}) { + const text = (await getTranslations(locale)).Events; + + // Normalize multi-select params + const categories = toArray(searchParams.category).filter(Boolean) as Cat[]; + const startDate = parseDate(searchParams.start); + const endDate = parseDate(searchParams.end); + const query = (searchParams.q ?? '').trim().toLowerCase(); + + // Build base query - fetch only initial batch + let raw = await db.query.events.findMany({ + where: (e, { and, gte, lte }) => + and( + startDate ? gte(e.startDate, startDate.toISOString()) : undefined, + endDate ? lte(e.startDate, endDate.toISOString()) : undefined + ), + orderBy: (e) => [desc(e.startDate)], + limit: INITIAL_BATCH_SIZE + 1, // +1 to check if there are more + }); + + console.log(raw); + + + // Category filter (multi) - check if event has ANY of the selected categories + if (categories.length) { + raw = raw.filter((e) => + e.categories.some((cat) => categories.includes(cat as Cat)) + ); + } + + // Text search (title, description, location, and categories) + if (query) { + raw = raw.filter( + (e) => + e.title.toLowerCase().includes(query.toLowerCase()) || + (e.description?.toLowerCase().includes(query.toLowerCase()) ?? false) || + (e.location?.toLowerCase().includes(query.toLowerCase()) ?? false) || + e.categories.some((cat) => + cat.toLowerCase().includes(query.toLowerCase()) + ) + ); + } + + // Check if there are more items + const hasMore = raw.length > INITIAL_BATCH_SIZE; + const initialItems = hasMore ? raw.slice(0, INITIAL_BATCH_SIZE) : raw; + const initialCursor = hasMore + ? initialItems[initialItems.length - 1]?.startDate ?? null + : null; + + // Serialize for client component + const serializedItems: EventItem[] = initialItems.map((e) => ({ + id: e.id, + title: e.title, + description: e.description, + categories: e.categories, + startDate: e.startDate, + endDate: e.endDate, + time: e.time, + location: e.location, + locationUrl: e.locationUrl, + images: e.images, + documents: e.documents, + })); + + // Build filter params for the client component + const filterParams = { + categories: categories.length ? categories : undefined, + start: searchParams.start, + end: searchParams.end, + query: query || undefined, + }; + + return ( + <> + +
    + {/* Desktop Sidebar - hidden on mobile */} + + + {/* Main Content */} +
    + {/* Search + Mobile Filters */} + + }> + + + + {/* Mobile Filters Button - shows on < xl */} +
    + }> + + +
    +
    + + {/* Events List */} +
    + }> + + +
    +
    +
    + + ); +} + +/* ---------------------- Filter Section Component ---------------------- */ +function FilterSection({ + label, + children, +}: { + label: string; + children: React.ReactNode; }) { - return ; + return ( +
    +
    +

    {label}

    +
    + {children} +
    + ); +} + +/* ---------------------- Helpers ---------------------- */ +function toArray(v: string | string[] | undefined): string[] { + return Array.isArray(v) ? v : v ? [v] : []; +} + +function parseDate(d?: string) { + if (!d) return undefined; + const date = new Date(d); + return isNaN(date.getTime()) ? undefined : date; +} + +function buildHref(locale: string, updates: Record): string { + const params = new URLSearchParams(); + + Object.entries(updates).forEach(([k, v]) => { + if (v === undefined || (Array.isArray(v) && v.length === 0)) { + return; + } + + if (Array.isArray(v)) { + v.forEach((item) => { + if (item) params.append(k, String(item)); + }); + } else { + params.set(k, String(v)); + } + }); + + const qs = params.toString(); + return `/${locale}/events${qs ? `?${qs}` : ''}`; } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 8367b4246..0c03cf2e2 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -14,7 +14,7 @@ import { import Heading from '~/components/heading'; import MessageCard from '~/components/message-card'; import { getTranslations } from '~/i18n/translations'; -import { type events } from '~/server/db'; +import { type eventCategoryEnum } from '~/server/db'; import Events from './events'; @@ -28,10 +28,7 @@ export default async function Home({ params: { locale: string }; searchParams: { notificationCategory?: NotificationCategory; - eventsCategory?: - | (typeof events.category.enumValues)[number] - | 'recents' - | 'featured'; + eventsCategory?: (typeof eventCategoryEnum.enumValues)[number] | 'featured'; }; }) { const text = (await getTranslations(locale)).Main; diff --git a/components/ui/dialog.tsx b/components/ui/dialog.tsx index 037e6edbb..7c858a9e6 100644 --- a/components/ui/dialog.tsx +++ b/components/ui/dialog.tsx @@ -21,7 +21,7 @@ const DialogOverlay = React.forwardRef< + and( + lt(e.startDate, cursorDate.toISOString()), // Cursor-based pagination + startDate ? gte(e.startDate, startDate.toISOString()) : undefined, + endDate ? lte(e.startDate, endDate.toISOString()) : undefined + ), + orderBy: (e) => [desc(e.startDate)], + limit: BATCH_SIZE + 1, // +1 to check if more exist + }); + + // Apply category filter - check if event has ANY of the selected categories + if (categories.length) { + raw = raw.filter((e) => + e.categories.some((cat) => categories.includes(cat as Cat)) + ); + } + + // Apply text search (title, description, location, categories) + if (query) { + const q = query.toLowerCase(); + raw = raw.filter( + (e) => + e.title.toLowerCase().includes(q) || + (e.description?.toLowerCase().includes(q) ?? false) || + (e.location?.toLowerCase().includes(q) ?? false) || + e.categories.some((cat) => cat.toLowerCase().includes(q)) + ); + } + + // Check if there are more items + const hasMore = raw.length > BATCH_SIZE; + const items = hasMore ? raw.slice(0, BATCH_SIZE) : raw; + const nextCursor = hasMore + ? items[items.length - 1]?.startDate ?? null + : null; + + // Serialize for client + return { + items: items.map((e) => ({ + id: e.id, + title: e.title, + description: e.description, + categories: e.categories, + startDate: e.startDate, + endDate: e.endDate, + time: e.time, + location: e.location, + locationUrl: e.locationUrl, + images: e.images, + documents: e.documents, + })), + cursor: nextCursor, + hasMore, + }; +} diff --git a/server/actions/index.ts b/server/actions/index.ts index 7f401dd34..270d800db 100644 --- a/server/actions/index.ts +++ b/server/actions/index.ts @@ -1,2 +1,3 @@ +export * from './events'; export * from './faculty-profile'; export * from './notifications'; diff --git a/server/db/schema/events.schema.ts b/server/db/schema/events.schema.ts index 23e493bf2..13444bbc8 100644 --- a/server/db/schema/events.schema.ts +++ b/server/db/schema/events.schema.ts @@ -1,39 +1,67 @@ -import { pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; +import { check, pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; import { clubs } from './clubs.schema'; +export const eventCategoryEnum = pgEnum('event_category', [ + 'academic', + 'technical', + 'cultural', + 'sports', + 'clubs-societies', + 'achievements', + 'placements', + 'outreach', + 'miscellaneous', + 'campus-highlights', +]); + export const events = pgTable( 'events', (t) => ({ id: t.serial('id').primaryKey(), title: t.varchar('title', { length: 256 }).unique().notNull(), description: t.text('description'), - category: t - .varchar('category', { - enum: ['student', 'faculty'], - }) - .notNull(), + categories: eventCategoryEnum('categories') + .array() + .notNull() + .default(sql`'{}'::event_category[]`), isFeatured: t.boolean('is_featured').default(false).notNull(), startDate: t.date('start_date').notNull(), - endDate: t.date('end_date').notNull(), + endDate: t.date('end_date'), // Optional - null means single-day event + time: t.varchar('time', { length: 32 }), // Optional - e.g. "4:30 PM" + location: t.varchar('location', { length: 256 }), + locationUrl: t.varchar('location_url', { length: 512 }), clubId: t.integer('club_id').references(() => clubs.id), images: t .text('images') .array() .notNull() .default(sql`'{}'::text[]`), + documents: t + .text('documents') + .array() + .notNull() + .default(sql`'{}'::text[]`), createdAt: t.timestamp('created_at').defaultNow().notNull(), updatedAt: t .timestamp('updated_at') .$onUpdate(() => new Date()) .notNull(), }), - (events) => { - return { - eventsTitleIndex: uniqueIndex('events_title_idx').on(events.title), - }; - } + (events) => ({ + eventsTitleIndex: uniqueIndex('events_title_idx').on(events.title), + // Both location fields must be filled together or both null + locationCheck: check( + 'location_check', + sql`(${events.location} IS NULL AND ${events.locationUrl} IS NULL) OR (${events.location} IS NOT NULL AND ${events.locationUrl} IS NOT NULL)` + ), + // endDate must be different from startDate (if endDate is provided) + dateCheck: check( + 'date_check', + sql`${events.endDate} IS NULL OR ${events.endDate} > ${events.startDate}` + ), + }) ); export const eventsRelations = relations(events, ({ one }) => ({ diff --git a/server/db/schema/faculty.schema.ts b/server/db/schema/faculty.schema.ts index 32477a620..af442bd87 100644 --- a/server/db/schema/faculty.schema.ts +++ b/server/db/schema/faculty.schema.ts @@ -45,7 +45,7 @@ export const faculty = pgTable( scopusId: t.text(), areasOfInterest: t.text().array().default([]), }), - (table) => [uniqueIndex('faculty_employee_id_idx').on(table.employeeId)] + // (table) => [uniqueIndex('faculty_employee_id_idx').on(table.employeeId)] ); // IPR From cf24730ccae968da95d5f49fbd4a08fbb4074d8f Mon Sep 17 00:00:00 2001 From: Aryawart-kathpal Date: Sun, 18 Jan 2026 02:16:12 +0530 Subject: [PATCH 17/73] fix: Added junction tables for Notifications --- app/[locale]/academics/page.tsx | 2 +- .../notifications/NotificationModal.tsx | 158 +----------------- app/[locale]/notifications/page.tsx | 35 +++- .../clubs/[display_name]/page.tsx | 2 +- .../notification-item-with-modal.tsx | 2 +- .../notifications/notification-modal.tsx | 156 +++++++++++++++++ .../notifications/notifications-panel.tsx | 145 ++++++++++++---- server/actions/notifications.ts | 103 ++++++++++-- server/db/schema/clubs.schema.ts | 4 +- server/db/schema/departments.schema.ts | 2 + server/db/schema/hostels.schema.ts | 2 + server/db/schema/notifications.schema.ts | 154 +++++++++++------ 12 files changed, 505 insertions(+), 260 deletions(-) create mode 100644 components/notifications/notification-modal.tsx diff --git a/app/[locale]/academics/page.tsx b/app/[locale]/academics/page.tsx index 4e736f5ee..0263737bc 100644 --- a/app/[locale]/academics/page.tsx +++ b/app/[locale]/academics/page.tsx @@ -262,4 +262,4 @@ export default async function Academics({ ); -} \ No newline at end of file +} diff --git a/app/[locale]/notifications/NotificationModal.tsx b/app/[locale]/notifications/NotificationModal.tsx index d415caf9f..9bfb7c48c 100644 --- a/app/[locale]/notifications/NotificationModal.tsx +++ b/app/[locale]/notifications/NotificationModal.tsx @@ -1,156 +1,2 @@ -'use client'; - -import { useEffect, useState } from 'react'; -import { MdCalendarToday, MdOpenInNew } from 'react-icons/md'; - -import { Dialog, DialogContent, ScrollArea } from '~/components/ui'; -import Loading from '~/components/loading'; -import { - getNotificationById, - type NotificationDetails, -} from '~/server/actions/notifications'; - -interface NotificationModalProps { - notificationId: number | null; - onClose: () => void; - locale: string; -} - -export function NotificationModal({ - notificationId, - onClose, - locale, -}: NotificationModalProps) { - const [notification, setNotification] = useState( - null - ); - const [isLoading, setIsLoading] = useState(false); - - useEffect(() => { - if (notificationId === null) { - setNotification(null); - return; - } - - setIsLoading(true); - getNotificationById(notificationId) - .then((data) => { - setNotification(data); - }) - .catch((error) => { - console.error('Failed to fetch notification:', error); - setNotification(null); - }) - .finally(() => { - setIsLoading(false); - }); - }, [notificationId]); - - const formatDate = (dateStr: string) => { - const date = new Date(dateStr); - return date.toLocaleDateString(locale, { - day: 'numeric', - month: 'long', - year: 'numeric', - }); - }; - - // Extract filename from URL for document button label (without extension) - const getDocumentName = (url: string, index: number) => { - try { - const urlObj = new URL(url); - const pathname = urlObj.pathname; - const filename = pathname.split('/').pop(); - if (filename) { - // Decode URI and remove extension - const decoded = decodeURIComponent(filename); - const nameWithoutExt = decoded.replace(/\.[^/.]+$/, ''); - return nameWithoutExt || decoded; - } - } catch { - // If URL parsing fails, use generic name - } - return `Document ${index + 1}`; - }; - - return ( - !open && onClose()} - > - - {isLoading ? ( -
    - -
    - ) : notification ? ( - <> - {/* Header with date and close button */} -
    - - - {formatDate(notification.createdAt)} - -
    - - {/* Title */} -

    - {notification.title} -

    - - {/* Content */} - {notification.content && ( -
    - -

    - {notification.content} -

    -
    -
    - )} - - {/* Documents */} - {notification.documents.length > 0 && ( -
    -
    2 ? 'h-32 sm:h-auto' : '' - } ${notification.documents.length > 6 ? 'sm:h-52' : ''}`} - > - -
    - {notification.documents.map((doc, index) => ( - - - {getDocumentName(doc, index)} - - - - ))} -
    -
    -
    -
    - )} - - ) : ( -
    -

    Notification not found

    -
    - )} -
    -
    - ); -} +// Re-export from components for backward compatibility +export { NotificationModal } from '~/components/notifications/notification-modal'; diff --git a/app/[locale]/notifications/page.tsx b/app/[locale]/notifications/page.tsx index 637e07f8f..693282cb6 100644 --- a/app/[locale]/notifications/page.tsx +++ b/app/[locale]/notifications/page.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import React from 'react'; -import { desc } from 'drizzle-orm'; +import { desc, inArray } from 'drizzle-orm'; import { getTranslations } from '~/i18n/translations'; import { db } from '~/server/db'; @@ -8,8 +8,10 @@ import { cn } from '~/lib/utils'; import ImageHeader from '~/components/image-header'; import { Button } from '~/components/buttons'; import { ScrollArea } from '~/components/ui'; -import { notifications as notificationsSchema } from '~/server/db'; -import { notificationCategoryEnum } from '~/server/db/schema/notifications.schema'; +import { + notificationCategoryEnum, + notificationDepartments, +} from '~/server/db/schema/notifications.schema'; import { type NotificationItem } from '~/server/actions/notifications'; import { DateRangeForm } from './DateRangeForm'; @@ -58,12 +60,32 @@ export default async function NotificationsPage({ .map((d) => d.id) : []; + // Get notification IDs that match department filter via junction table + let filteredNotificationIds: number[] | undefined; + if (deptIds.length) { + const deptMatches = await db + .selectDistinct({ + notificationId: notificationDepartments.notificationId, + }) + .from(notificationDepartments) + .where(inArray(notificationDepartments.departmentId, deptIds)); + filteredNotificationIds = deptMatches.map((m) => m.notificationId); + + // If no notifications match the department filter, return empty + if (filteredNotificationIds.length === 0) { + filteredNotificationIds = [-1]; // Use impossible ID to return no results + } + } + // Build base query - fetch only initial batch let raw = await db.query.notifications.findMany({ where: (n, { and, gte, lte }) => and( startDate ? gte(n.createdAt, startDate) : undefined, - endDate ? lte(n.createdAt, endDate) : undefined + endDate ? lte(n.createdAt, endDate) : undefined, + filteredNotificationIds + ? inArray(n.id, filteredNotificationIds) + : undefined ), orderBy: (n) => [desc(n.createdAt)], limit: INITIAL_BATCH_SIZE + 1, // +1 to check if there are more @@ -76,11 +98,6 @@ export default async function NotificationsPage({ ); } - // Department filter (multi via foreign key, if departmentId present) - if (deptIds.length) { - raw = raw.filter((n) => n.departmentId && deptIds.includes(n.departmentId)); - } - // Text search (title and content) if (query) { raw = raw.filter( diff --git a/app/[locale]/student-activities/clubs/[display_name]/page.tsx b/app/[locale]/student-activities/clubs/[display_name]/page.tsx index b297e82c1..5399ce059 100644 --- a/app/[locale]/student-activities/clubs/[display_name]/page.tsx +++ b/app/[locale]/student-activities/clubs/[display_name]/page.tsx @@ -366,7 +366,7 @@ export default async function Club({ /> void; + locale: string; +} + +export function NotificationModal({ + notificationId, + onClose, + locale, +}: NotificationModalProps) { + const [notification, setNotification] = useState( + null + ); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (notificationId === null) { + setNotification(null); + return; + } + + setIsLoading(true); + getNotificationById(notificationId) + .then((data) => { + setNotification(data); + }) + .catch((error) => { + console.error('Failed to fetch notification:', error); + setNotification(null); + }) + .finally(() => { + setIsLoading(false); + }); + }, [notificationId]); + + const formatDate = (dateStr: string) => { + const date = new Date(dateStr); + return date.toLocaleDateString(locale, { + day: 'numeric', + month: 'long', + year: 'numeric', + }); + }; + + // Extract filename from URL for document button label (without extension) + const getDocumentName = (url: string, index: number) => { + try { + const urlObj = new URL(url); + const pathname = urlObj.pathname; + const filename = pathname.split('/').pop(); + if (filename) { + // Decode URI and remove extension + const decoded = decodeURIComponent(filename); + const nameWithoutExt = decoded.replace(/\.[^/.]+$/, ''); + return nameWithoutExt || decoded; + } + } catch { + // If URL parsing fails, use generic name + } + return `Document ${index + 1}`; + }; + + return ( + !open && onClose()} + > + + {isLoading ? ( +
    + +
    + ) : notification ? ( + <> + {/* Header with date and close button */} +
    + + + {formatDate(notification.createdAt)} + +
    + + {/* Title */} +

    + {notification.title} +

    + + {/* Content */} + {notification.content && ( +
    + +

    + {notification.content} +

    +
    +
    + )} + + {/* Documents */} + {notification.documents.length > 0 && ( +
    +
    2 ? 'h-32 sm:h-auto' : '' + } ${notification.documents.length > 6 ? 'sm:h-52' : ''}`} + > + +
    + {notification.documents.map((doc, index) => ( + + + {getDocumentName(doc, index)} + + + + ))} +
    +
    +
    +
    + )} + + ) : ( +
    +

    Notification not found

    +
    + )} +
    +
    + ); +} diff --git a/components/notifications/notifications-panel.tsx b/components/notifications/notifications-panel.tsx index e55a2c1b0..0561c4fb9 100644 --- a/components/notifications/notifications-panel.tsx +++ b/components/notifications/notifications-panel.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; import { Suspense } from 'react'; -import { arrayOverlaps } from 'drizzle-orm'; +import { arrayOverlaps, inArray } from 'drizzle-orm'; import { Button } from '~/components/buttons'; import Loading from '~/components/loading'; @@ -8,7 +8,12 @@ import { NotificationItemWithModal } from '~/components/notifications/notificati import { ScrollArea } from '~/components/ui'; import { getTranslations } from '~/i18n/translations'; import { cn, groupBy } from '~/lib/utils'; -import { db, notifications as notificationsTable } from '~/server/db'; +import { db, type notifications as notificationsTable } from '~/server/db'; +import { + notificationClubs, + notificationDepartments, + notificationHostels, +} from '~/server/db/schema'; type NotificationCategory = (typeof notificationsTable.categories.enumValues)[number]; @@ -19,12 +24,12 @@ export interface NotificationsPanelProps { locale: string; /** Filter by notification category (matches if notification has this category) */ category?: NotificationCategory; - /** Filter by club ID */ - clubId?: number; - /** Filter by department ID */ - departmentId?: number; - /** Filter by hostel ID */ - hostelId?: number; + /** Filter by club IDs (matches if notification belongs to any of these clubs) */ + clubIds?: number[]; + /** Filter by department IDs (matches if notification belongs to any of these departments) */ + departmentIds?: number[]; + /** Filter by hostel IDs (matches if notification belongs to any of these hostels) */ + hostelIds?: number[]; /** Filter by education type (ug, pg, phd) */ educationType?: EducationType; /** Filter notifications created on or after this date */ @@ -44,9 +49,9 @@ export interface NotificationsPanelProps { export default async function NotificationsPanel({ locale, category, - clubId, - departmentId, - hostelId, + clubIds, + departmentIds, + hostelIds, educationType, startDate, endDate, @@ -56,7 +61,7 @@ export default async function NotificationsPanel({ viewAllText, }: NotificationsPanelProps) { const text = (await getTranslations(locale)).Notifications; - const filterKey = `${category}-${clubId}-${departmentId}-${hostelId}-${educationType}-${startDate?.toISOString()}-${endDate?.toISOString()}`; + const filterKey = `${category}-${clubIds?.join(',')}-${departmentIds?.join(',')}-${hostelIds?.join(',')}-${educationType}-${startDate?.toISOString()}-${endDate?.toISOString()}`; return (
    { + // Get notification IDs that match junction table filters + let filteredNotificationIds: number[] | undefined; + + if (departmentIds?.length) { + const deptMatches = await db + .selectDistinct({ + notificationId: notificationDepartments.notificationId, + }) + .from(notificationDepartments) + .where(inArray(notificationDepartments.departmentId, departmentIds)); + const deptNotificationIds = deptMatches.map((m) => m.notificationId); + + if (deptNotificationIds.length === 0) { + return ( +
  • + {noNotificationsText} +
  • + ); + } + filteredNotificationIds = deptNotificationIds; + } + + if (clubIds?.length) { + const clubMatches = await db + .selectDistinct({ notificationId: notificationClubs.notificationId }) + .from(notificationClubs) + .where(inArray(notificationClubs.clubId, clubIds)); + const clubNotificationIds = clubMatches.map((m) => m.notificationId); + + if (clubNotificationIds.length === 0) { + return ( +
  • + {noNotificationsText} +
  • + ); + } + + if (filteredNotificationIds) { + filteredNotificationIds = filteredNotificationIds.filter((id) => + clubNotificationIds.includes(id) + ); + if (filteredNotificationIds.length === 0) { + return ( +
  • + {noNotificationsText} +
  • + ); + } + } else { + filteredNotificationIds = clubNotificationIds; + } + } + + if (hostelIds?.length) { + const hostelMatches = await db + .selectDistinct({ notificationId: notificationHostels.notificationId }) + .from(notificationHostels) + .where(inArray(notificationHostels.hostelId, hostelIds)); + const hostelNotificationIds = hostelMatches.map((m) => m.notificationId); + + if (hostelNotificationIds.length === 0) { + return ( +
  • + {noNotificationsText} +
  • + ); + } + + if (filteredNotificationIds) { + filteredNotificationIds = filteredNotificationIds.filter((id) => + hostelNotificationIds.includes(id) + ); + if (filteredNotificationIds.length === 0) { + return ( +
  • + {noNotificationsText} +
  • + ); + } + } else { + filteredNotificationIds = hostelNotificationIds; + } + } + const notifications = ( await db.query.notifications.findMany({ where: (notification, { eq, and, gte, lte }) => { const conditions = []; if (category) { - conditions.push( - arrayOverlaps(notification.categories, [category]) - ); - } - if (clubId !== undefined) { - conditions.push(eq(notification.clubId, clubId)); - } - if (departmentId !== undefined) { - conditions.push(eq(notification.departmentId, departmentId)); + conditions.push(arrayOverlaps(notification.categories, [category])); } - if (hostelId !== undefined) { - conditions.push(eq(notification.hostelId, hostelId)); + if (filteredNotificationIds) { + conditions.push(inArray(notification.id, filteredNotificationIds)); } if (educationType) { conditions.push(eq(notification.educationType, educationType)); diff --git a/server/actions/notifications.ts b/server/actions/notifications.ts index ab75aa0ea..def608cf3 100644 --- a/server/actions/notifications.ts +++ b/server/actions/notifications.ts @@ -1,10 +1,15 @@ 'use server'; -import { and, desc, gte, lt, lte } from 'drizzle-orm'; +import { and, desc, gte, inArray, lt, lte } from 'drizzle-orm'; import { redirect } from 'next/navigation'; import { db } from '~/server/db'; -import { notifications } from '~/server/db/schema'; +import { + notificationClubs, + notificationDepartments, + notificationHostels, + notifications, +} from '~/server/db/schema'; const BATCH_SIZE = 20; @@ -26,6 +31,8 @@ export interface LoadMoreParams { categories?: string[]; departments?: string[]; departmentIds?: number[]; + clubIds?: number[]; + hostelIds?: number[]; start?: string; end?: string; query?: string; @@ -34,18 +41,98 @@ export interface LoadMoreParams { export async function loadMoreNotifications( params: LoadMoreParams ): Promise { - const { cursor, categories, departmentIds, start, end, query } = params; + const { + cursor, + categories, + departmentIds, + clubIds, + hostelIds, + start, + end, + query, + } = params; const cursorDate = cursor ? new Date(cursor) : undefined; const startDate = start ? new Date(start) : undefined; const endDate = end ? new Date(end) : undefined; - // Build conditions + // Build base conditions const conditions = []; if (startDate) conditions.push(gte(notifications.createdAt, startDate)); if (endDate) conditions.push(lte(notifications.createdAt, endDate)); if (cursorDate) conditions.push(lt(notifications.createdAt, cursorDate)); + // Get notification IDs that match department/club/hostel filters via junction tables + let filteredNotificationIds: number[] | undefined; + + if (departmentIds?.length) { + const deptMatches = await db + .selectDistinct({ + notificationId: notificationDepartments.notificationId, + }) + .from(notificationDepartments) + .where(inArray(notificationDepartments.departmentId, departmentIds)); + const deptNotificationIds = deptMatches.map((m) => m.notificationId); + + if (deptNotificationIds.length === 0) { + return { items: [], nextCursor: null, hasMore: false }; + } + filteredNotificationIds = deptNotificationIds; + } + + if (clubIds?.length) { + const clubMatches = await db + .selectDistinct({ notificationId: notificationClubs.notificationId }) + .from(notificationClubs) + .where(inArray(notificationClubs.clubId, clubIds)); + const clubNotificationIds = clubMatches.map((m) => m.notificationId); + + if (clubNotificationIds.length === 0) { + return { items: [], nextCursor: null, hasMore: false }; + } + + // Intersect with existing filter + if (filteredNotificationIds) { + filteredNotificationIds = filteredNotificationIds.filter((id) => + clubNotificationIds.includes(id) + ); + if (filteredNotificationIds.length === 0) { + return { items: [], nextCursor: null, hasMore: false }; + } + } else { + filteredNotificationIds = clubNotificationIds; + } + } + + if (hostelIds?.length) { + const hostelMatches = await db + .selectDistinct({ notificationId: notificationHostels.notificationId }) + .from(notificationHostels) + .where(inArray(notificationHostels.hostelId, hostelIds)); + const hostelNotificationIds = hostelMatches.map((m) => m.notificationId); + + if (hostelNotificationIds.length === 0) { + return { items: [], nextCursor: null, hasMore: false }; + } + + // Intersect with existing filter + if (filteredNotificationIds) { + filteredNotificationIds = filteredNotificationIds.filter((id) => + hostelNotificationIds.includes(id) + ); + if (filteredNotificationIds.length === 0) { + return { items: [], nextCursor: null, hasMore: false }; + } + } else { + filteredNotificationIds = hostelNotificationIds; + } + } + + // Add junction table filter to conditions + if (filteredNotificationIds) { + conditions.push(inArray(notifications.id, filteredNotificationIds)); + } + // Fetch batch + 1 to check if there are more let results = await db.query.notifications.findMany({ where: conditions.length ? and(...conditions) : undefined, @@ -53,19 +140,13 @@ export async function loadMoreNotifications( limit: BATCH_SIZE + 1, }); - // Apply in-memory filters (category, department, text search) + // Apply in-memory filters (category, text search) if (categories?.length) { results = results.filter((n) => n.categories.some((cat) => categories.includes(cat)) ); } - if (departmentIds?.length) { - results = results.filter( - (n) => n.departmentId && departmentIds.includes(n.departmentId) - ); - } - if (query) { const lowerQuery = query.toLowerCase(); results = results.filter( diff --git a/server/db/schema/clubs.schema.ts b/server/db/schema/clubs.schema.ts index ea74bdb39..a89b85b95 100644 --- a/server/db/schema/clubs.schema.ts +++ b/server/db/schema/clubs.schema.ts @@ -5,7 +5,7 @@ import { clubMembers } from './club-members.schema'; import { clubSocials } from './club-socials.schema'; import { departments } from './departments.schema'; import { events } from './events.schema'; -import { notifications } from './notifications.schema'; +import { notificationClubs } from './notifications.schema'; import { persons } from './persons.schema'; import { clubFacultyHeads } from './club-faculty-heads.schema'; @@ -45,6 +45,6 @@ export const clubsRelations = relations(clubs, ({ many, one }) => ({ fields: [clubs.departmentId], references: [departments.id], }), - clubNotifications: many(notifications), + notificationClubs: many(notificationClubs), clubFacultyHeads: many(clubFacultyHeads), })); diff --git a/server/db/schema/departments.schema.ts b/server/db/schema/departments.schema.ts index 9be91d15d..119312c59 100644 --- a/server/db/schema/departments.schema.ts +++ b/server/db/schema/departments.schema.ts @@ -2,6 +2,7 @@ import { relations } from 'drizzle-orm'; import { pgTable } from 'drizzle-orm/pg-core'; import { clubs, courses, doctorates, faculty, majors, staff } from '.'; +import { notificationDepartments } from './notifications.schema'; export const departments = pgTable('departments', (t) => ({ id: t.smallserial().primaryKey(), @@ -24,4 +25,5 @@ export const departmentsRelations = relations(departments, ({ many }) => ({ faculty: many(faculty), majors: many(majors), staff: many(staff), + notificationDepartments: many(notificationDepartments), })); diff --git a/server/db/schema/hostels.schema.ts b/server/db/schema/hostels.schema.ts index 177da0b45..3b5414651 100644 --- a/server/db/schema/hostels.schema.ts +++ b/server/db/schema/hostels.schema.ts @@ -2,6 +2,7 @@ import { relations } from 'drizzle-orm'; import { pgTable } from 'drizzle-orm/pg-core'; import { faculty, staff } from '.'; +import { notificationHostels } from './notifications.schema'; export const hostels = pgTable('hostels', (t) => ({ id: t.serial().primaryKey(), @@ -25,6 +26,7 @@ export const hostels = pgTable('hostels', (t) => ({ export const hostelsRelations = relations(hostels, ({ many }) => ({ hostelStaff: many(hostelStaff), hostelFaculty: many(hostelFaculty), + notificationHostels: many(notificationHostels), })); export const hostelStaff = pgTable('hostel_staff', (t) => ({ diff --git a/server/db/schema/notifications.schema.ts b/server/db/schema/notifications.schema.ts index 4d74f797c..5205b4bee 100644 --- a/server/db/schema/notifications.schema.ts +++ b/server/db/schema/notifications.schema.ts @@ -1,4 +1,4 @@ -import { check, pgEnum, pgTable, uniqueIndex } from 'drizzle-orm/pg-core'; +import { pgEnum, pgTable, primaryKey, uniqueIndex } from 'drizzle-orm/pg-core'; import { relations, sql } from 'drizzle-orm'; import { clubs } from './clubs.schema'; @@ -31,7 +31,6 @@ export const notifications = pgTable( title: t.varchar('title', { length: 256 }).unique().notNull(), content: t.text('content'), - // NEW - array of categories categories: notificationCategoryEnum('categories') .array() .notNull() @@ -50,58 +49,119 @@ export const notifications = pgTable( .timestamp('updated_at') .$onUpdate(() => new Date()) .notNull(), - clubId: t.integer('club_id').references(() => clubs.id), - departmentId: t.integer('department_id').references(() => departments.id), - hostelId: t.integer('hostel_id').references(() => hostels.id), + // NOTE: clubId, departmentId, hostelId removed - now using junction tables }), (n) => ({ notificationsTitleIndex: uniqueIndex('notifications_title_idx').on(n.title), - clubRequiredForStudent: check( - 'club_required_for_student', - sql`( - ('student-activities' = ANY(${n.categories})) - OR - (NOT ('student-activities' = ANY(${n.categories})) AND ${n.clubId} IS NULL) - )` - ), - educationTypeRequiredForAcademicAdmission: check( - 'education_type_required_for_academic_admission', - sql`( - ('academic' = ANY(${n.categories}) OR 'admission' = ANY(${n.categories})) - OR - (NOT ('academic' = ANY(${n.categories})) AND NOT ('admission' = ANY(${n.categories})) AND ${n.educationType} IS NULL) - )` - ), - hostelRequiredForHostel: check( - 'hostel_required_for_hostel', - sql`( - ('hostel' = ANY(${n.categories})) - OR - (NOT ('hostel' = ANY(${n.categories})) AND ${n.hostelId} IS NULL) - )` - ), - departmentAllowedOnlyWhenRelevant: check( - 'department_allowed_only_when_relevant', - sql`( - ('academic' = ANY(${n.categories}) OR 'workshop' = ANY(${n.categories}) OR 'administration' = ANY(${n.categories}) OR 'recruitment' = ANY(${n.categories}) OR 'admission' = ANY(${n.categories}) OR 'faculty' = ANY(${n.categories}) OR 'research' = ANY(${n.categories}) OR 'examination' = ANY(${n.categories}) OR 'result' = ANY(${n.categories})) - OR - (${n.departmentId} IS NULL) - )` - ), + // NOTE: Check constraints removed - enforced at application level since + // we now use junction tables for clubs, departments, and hostels }) ); -export const notificationsRelations = relations(notifications, ({ one }) => ({ - club: one(clubs, { - fields: [notifications.clubId], - references: [clubs.id], +// ===================== JUNCTION TABLES ===================== + +// Junction table: notifications <-> departments (many-to-many) +export const notificationDepartments = pgTable( + 'notification_departments', + (t) => ({ + notificationId: t + .integer('notification_id') + .notNull() + .references(() => notifications.id, { onDelete: 'cascade' }), + departmentId: t + .integer('department_id') + .notNull() + .references(() => departments.id, { onDelete: 'cascade' }), }), - department: one(departments, { - fields: [notifications.departmentId], - references: [departments.id], + (table) => ({ + pk: primaryKey({ columns: [table.notificationId, table.departmentId] }), + }) +); + +// Junction table: notifications <-> clubs (many-to-many) +export const notificationClubs = pgTable( + 'notification_clubs', + (t) => ({ + notificationId: t + .integer('notification_id') + .notNull() + .references(() => notifications.id, { onDelete: 'cascade' }), + clubId: t + .integer('club_id') + .notNull() + .references(() => clubs.id, { onDelete: 'cascade' }), }), - hostel: one(hostels, { - fields: [notifications.hostelId], - references: [hostels.id], + (table) => ({ + pk: primaryKey({ columns: [table.notificationId, table.clubId] }), + }) +); + +// Junction table: notifications <-> hostels (many-to-many) +export const notificationHostels = pgTable( + 'notification_hostels', + (t) => ({ + notificationId: t + .integer('notification_id') + .notNull() + .references(() => notifications.id, { onDelete: 'cascade' }), + hostelId: t + .integer('hostel_id') + .notNull() + .references(() => hostels.id, { onDelete: 'cascade' }), }), + (table) => ({ + pk: primaryKey({ columns: [table.notificationId, table.hostelId] }), + }) +); + +// ===================== RELATIONS ===================== + +// Junction table relations +export const notificationDepartmentsRelations = relations( + notificationDepartments, + ({ one }) => ({ + notification: one(notifications, { + fields: [notificationDepartments.notificationId], + references: [notifications.id], + }), + department: one(departments, { + fields: [notificationDepartments.departmentId], + references: [departments.id], + }), + }) +); + +export const notificationClubsRelations = relations( + notificationClubs, + ({ one }) => ({ + notification: one(notifications, { + fields: [notificationClubs.notificationId], + references: [notifications.id], + }), + club: one(clubs, { + fields: [notificationClubs.clubId], + references: [clubs.id], + }), + }) +); + +export const notificationHostelsRelations = relations( + notificationHostels, + ({ one }) => ({ + notification: one(notifications, { + fields: [notificationHostels.notificationId], + references: [notifications.id], + }), + hostel: one(hostels, { + fields: [notificationHostels.hostelId], + references: [hostels.id], + }), + }) +); + +// Main notifications relations +export const notificationsRelations = relations(notifications, ({ many }) => ({ + notificationDepartments: many(notificationDepartments), + notificationClubs: many(notificationClubs), + notificationHostels: many(notificationHostels), })); From 35949c707ed2577d11fb96353ef2ee1c4c642230 Mon Sep 17 00:00:00 2001 From: Yashika Choudhary <161009245+yashika1221@users.noreply.github.com> Date: Sun, 18 Jan 2026 02:47:04 +0530 Subject: [PATCH 18/73] Fix/pagination generictable --- app/[locale]/academics/programmes/page.tsx | 12 +- .../institute/administration/page.tsx | 15 - app/[locale]/institute/cells/ipr/page.tsx | 2 - .../institute/hostels/[url_name]/page.tsx | 5 +- app/[locale]/institute/page.tsx | 2 - .../sections/central-workshop/page.tsx | 6 +- .../institute/sections/estate/page.tsx | 67 +- .../institute/sections/health-centre/page.tsx | 17 +- .../library/library-committee/page.tsx | 2 - .../membership-and-privileges/page.tsx | 2 - .../institute/sections/library/page.tsx | 2 - app/[locale]/research/page.tsx | 632 ++++-------------- components/ui/generic-table.tsx | 28 +- 13 files changed, 185 insertions(+), 607 deletions(-) diff --git a/app/[locale]/academics/programmes/page.tsx b/app/[locale]/academics/programmes/page.tsx index 924f70cdb..2d8e59ae7 100644 --- a/app/[locale]/academics/programmes/page.tsx +++ b/app/[locale]/academics/programmes/page.tsx @@ -180,9 +180,7 @@ export default async function Programmes({ @@ -202,9 +200,7 @@ export default async function Programmes({ @@ -217,9 +213,7 @@ export default async function Programmes({ diff --git a/app/[locale]/institute/administration/page.tsx b/app/[locale]/institute/administration/page.tsx index 2d580907c..acb0a4bdb 100644 --- a/app/[locale]/institute/administration/page.tsx +++ b/app/[locale]/institute/administration/page.tsx @@ -15,8 +15,6 @@ import ButtonGroup from '~/components/button-group'; import { getTranslations } from '~/i18n/translations'; // Fetches committee data from DB - cache for 1 hour export const revalidate = 3600; -import { db } from '~/server/db'; -import GenericTable from '~/components/ui/generic-table'; import Loading from '~/components/loading'; import { CardTitle } from '~/components/ui'; @@ -101,19 +99,6 @@ export default async function Administration({ {text.composition} - eq(member.committeeType, 'senate'), - orderBy: (member, { asc }) => [asc(member.serial)], - })} - currentPage={1} - getCount={Promise.resolve([])} - />
    diff --git a/app/[locale]/institute/hostels/[url_name]/page.tsx b/app/[locale]/institute/hostels/[url_name]/page.tsx index 701da9e0e..5e650d9d7 100644 --- a/app/[locale]/institute/hostels/[url_name]/page.tsx +++ b/app/[locale]/institute/hostels/[url_name]/page.tsx @@ -6,7 +6,7 @@ import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; -import { db, hostels } from '~/server/db'; +import { db } from '~/server/db'; // Fetches hostel data from DB - cache for 1 hour export const revalidate = 3600; @@ -171,8 +171,7 @@ export default async function Hostel({ email: person?.email, }; })} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName={index === 0 ? 'faculty-page' : 'staff-page'} /> ))} diff --git a/app/[locale]/institute/page.tsx b/app/[locale]/institute/page.tsx index 21583f226..ff850e711 100644 --- a/app/[locale]/institute/page.tsx +++ b/app/[locale]/institute/page.tsx @@ -216,8 +216,6 @@ export default async function Institute({ : row.nirfCertificate, dataFile: row.dataFileLink ? row.dataFileLink : row.dataFile, }))} - currentPage={1} - getCount={Promise.resolve([])} /> diff --git a/app/[locale]/institute/sections/central-workshop/page.tsx b/app/[locale]/institute/sections/central-workshop/page.tsx index f1b10c5d6..b697fb169 100644 --- a/app/[locale]/institute/sections/central-workshop/page.tsx +++ b/app/[locale]/institute/sections/central-workshop/page.tsx @@ -136,8 +136,7 @@ export default async function CentralWorkshop({ name, quantity, }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName={`${category}-page`} /> {categoryText.miscDetails && ( @@ -202,8 +201,7 @@ const StaffTable = async ({ name, designation, }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName="staff-page" /> ); }; diff --git a/app/[locale]/institute/sections/estate/page.tsx b/app/[locale]/institute/sections/estate/page.tsx index 974679ab7..fe3557db7 100644 --- a/app/[locale]/institute/sections/estate/page.tsx +++ b/app/[locale]/institute/sections/estate/page.tsx @@ -6,7 +6,6 @@ import React from 'react'; import { Suspense } from 'react'; import { MdArticle } from 'react-icons/md'; -import { Table, TableBody, TableCell, TableRow } from '~/components/ui'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; @@ -706,7 +705,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={committeeMembers} - currentPage={1} + pageParamName="bwcPage" getCount={Promise.resolve([])} /> @@ -727,7 +726,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={estateAffairsCommittee} - currentPage={1} + pageParamName="eacPage" getCount={Promise.resolve([])} /> @@ -741,7 +740,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={InspectionCommitteeMembers} - currentPage={1} + pageParamName="icPage" getCount={Promise.resolve([])} /> @@ -756,7 +755,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={SpaceAllocationCommitteeMembers} - currentPage={1} + pageParamName="sacPage" getCount={Promise.resolve([])} /> @@ -770,7 +769,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={ProgressCommitteeMembers} - currentPage={1} + pageParamName="pcPage" getCount={Promise.resolve([])} /> @@ -784,7 +783,7 @@ export default async function Estate({ { key: 'position', label: 'Position' }, ]} tableData={LicensingCommitteeMembers} - currentPage={1} + pageParamName="lcPage" getCount={Promise.resolve([])} /> @@ -799,7 +798,7 @@ export default async function Estate({ { key: 'role', label: 'Role' }, ]} tableData={HouseAllotmentCommitteeMembers} - currentPage={1} + pageParamName="hacPage" getCount={Promise.resolve([])} /> @@ -814,7 +813,7 @@ export default async function Estate({ { key: 'role', label: 'Role' }, ]} tableData={HouseAllotmentCommitteeMembers2} - currentPage={1} + pageParamName="hac2Page" getCount={Promise.resolve([])} /> @@ -834,7 +833,7 @@ export default async function Estate({ { key: 'value', label: 'Value' }, ]} tableData={areaDetails} - currentPage={1} + pageParamName="adPage" getCount={Promise.resolve([])} /> @@ -848,7 +847,7 @@ export default async function Estate({ { key: 'area', label: 'Area' }, ]} tableData={infrastructureDetails} - currentPage={1} + pageParamName="idPage" getCount={Promise.resolve([])} /> @@ -865,7 +864,7 @@ export default async function Estate({ details: item.details, area: item.area, }))} - currentPage={1} + pageParamName="aadPage" getCount={Promise.resolve([])} /> @@ -884,7 +883,7 @@ export default async function Estate({ type: item.type, area: item.area, }))} - currentPage={1} + pageParamName="hadPage" getCount={Promise.resolve([])} /> @@ -905,7 +904,7 @@ export default async function Estate({ capacity: item.capacity, quantity: item.quantity, }))} - currentPage={1} + pageParamName="hdPage" getCount={Promise.resolve([])} /> @@ -926,7 +925,7 @@ export default async function Estate({ capacity: item.capacity, quantity: item.quantity, }))} - currentPage={1} + pageParamName="ghdPage" getCount={Promise.resolve([])} /> @@ -945,7 +944,7 @@ export default async function Estate({ plinthArea: item.plinthArea, numberOfHouses: item.numberOfHouses, }))} - currentPage={1} + pageParamName="radPage" getCount={Promise.resolve([])} /> @@ -962,7 +961,7 @@ export default async function Estate({ facility: item.facility, area: item.area, }))} - currentPage={1} + pageParamName="sfdPage" getCount={Promise.resolve([])} /> @@ -979,10 +978,10 @@ export default async function Estate({ ({ + tableData={text.project.completed.map((project) => ({ project, }))} - currentPage={1} + pageParamName="cpPage" getCount={Promise.resolve([])} /> @@ -993,10 +992,10 @@ export default async function Estate({ ({ + tableData={text.project.ongoing.map((project) => ({ project, }))} - currentPage={1} + pageParamName="opPage" getCount={Promise.resolve([])} /> @@ -1007,10 +1006,10 @@ export default async function Estate({ ({ + tableData={text.project.future.map((project) => ({ project, }))} - currentPage={1} + pageParamName="fpPage" getCount={Promise.resolve([])} /> @@ -1024,19 +1023,15 @@ export default async function Estate({ id="seniority" text={text.headings[8].toUpperCase()} /> - - - {seniorityLinks.map((link, index) => ( - - - - {link.text} - - - - ))} - -
    +
    + {seniorityLinks.map((link, index) => ( + + ))} +
    ); diff --git a/app/[locale]/institute/sections/health-centre/page.tsx b/app/[locale]/institute/sections/health-centre/page.tsx index 35b84d464..3822bfb59 100644 --- a/app/[locale]/institute/sections/health-centre/page.tsx +++ b/app/[locale]/institute/sections/health-centre/page.tsx @@ -7,7 +7,6 @@ import Image from 'next/image'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; import { getTranslations } from '~/i18n/translations'; -import { db } from '~/server/db'; import GenericTable from '~/components/ui/generic-table'; import Loading from '~/components/loading'; @@ -18,10 +17,6 @@ export default async function HealthCentre({ }) { const text = (await getTranslations(locale)).Section.HealthCentre; - const section = (await db.query.sections.findFirst({ - where: (section, { eq }) => eq(section.urlName, 'health-centre'), - }))!; - const hospitalData = [ { srNo: '1', @@ -494,8 +489,7 @@ export default async function HealthCentre({ ) .join(', '), }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName="timings-page" /> @@ -520,8 +514,7 @@ export default async function HealthCentre({ role, tel, }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName="officers-page" /> }> @@ -537,8 +530,7 @@ export default async function HealthCentre({ designation, phone, }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName="staff-page" /> @@ -704,8 +696,7 @@ export default async function HealthCentre({ field, phone, }))} - currentPage={1} - getCount={Promise.resolve([])} + pageParamName="hospitals-page" /> diff --git a/app/[locale]/institute/sections/library/library-committee/page.tsx b/app/[locale]/institute/sections/library/library-committee/page.tsx index 05408375d..036170570 100644 --- a/app/[locale]/institute/sections/library/library-committee/page.tsx +++ b/app/[locale]/institute/sections/library/library-committee/page.tsx @@ -55,8 +55,6 @@ export default async function libraryCommittee({ generalDesignation: entry.faculty.designation, libraryCommitteeDesignation: entry.libraryCommitteeDesignation, }))} - currentPage={1} - getCount={Promise.resolve([])} /> diff --git a/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx b/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx index b72317648..7f6a07caa 100644 --- a/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx +++ b/app/[locale]/institute/sections/library/membership-and-privileges/page.tsx @@ -62,8 +62,6 @@ export default async function MembershipAndPrivileges({ { key: 'periodOfLoan', label: 'Period of Loan' }, ]} tableData={LoanTableData} - currentPage={1} - getCount={Promise.resolve([])} /> diff --git a/app/[locale]/institute/sections/library/page.tsx b/app/[locale]/institute/sections/library/page.tsx index 93fc46d4d..d27f44e96 100644 --- a/app/[locale]/institute/sections/library/page.tsx +++ b/app/[locale]/institute/sections/library/page.tsx @@ -271,8 +271,6 @@ export default async function Library({ { key: 'email', label: text.contactUs.email }, ]} tableData={contactUsData} - currentPage={1} - getCount={Promise.resolve([])} /> diff --git a/app/[locale]/research/page.tsx b/app/[locale]/research/page.tsx index 55c714579..5b6bc53e0 100644 --- a/app/[locale]/research/page.tsx +++ b/app/[locale]/research/page.tsx @@ -9,37 +9,16 @@ import { sql } from 'drizzle-orm'; import Heading from '~/components/heading'; import Loading from '~/components/loading'; import ImageHeader from '~/components/image-header'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; -import { PaginationWithLogic } from '~/components/pagination/pagination'; +import GenericTable from '~/components/ui/generic-table'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; import { db } from '~/server/db'; -import type { - copyrights, - designs, - mous, - patents, - researchAndConsultancy, - sponsoredResearchProjects, - sponsoredResearchProjectsFaculties, -} from '~/server/db/schema'; +import type { mous } from '~/server/db/schema'; -type PatentsTable = typeof patents.$inferSelect; -type CopyrightsTable = typeof copyrights.$inferSelect; -type DesignsTable = typeof designs.$inferSelect; -type ResearchAndConsultancyTable = typeof researchAndConsultancy.$inferSelect; type Moustable = typeof mous.$inferSelect; export default async function PatentsAndTechnology({ params: { locale }, - searchParams, }: { params: { locale: string }; searchParams?: { @@ -51,14 +30,6 @@ export default async function PatentsAndTechnology({ projectsPage?: string; }; }) { - // Individual page states for each table - const researchPage = Number(searchParams?.researchPage ?? 1); - const patentsPage = Number(searchParams?.patentsPage ?? 1); - const copyrightsPage = Number(searchParams?.copyrightsPage ?? 1); - const designsPage = Number(searchParams?.designsPage ?? 1); - const memorandumPage = Number(searchParams?.memorandumPage ?? 1); - const projectsPage = Number(searchParams?.projectsPage ?? 1); - const text = (await getTranslations(locale)).Research; const archiveLinks = [ @@ -108,7 +79,7 @@ export default async function PatentsAndTechnology({ orderBy: (rc) => sql`SUBSTRING(${rc.year}, 1, 4)::integer DESC`, } ); - const mous = await db.query.mous.findMany(); + const mous: { id: number; organization: string; signingDate: string }[] = []; const staticMemorandum: Moustable[] = mous; const formattedMemorandum = staticMemorandum.map((item) => { return { @@ -178,49 +149,26 @@ export default async function PatentsAndTechnology({ />
    -
    - - - - {[ - text.research.number, - text.research.faculty, - text.research.department, - text.research.totalJobs, - text.research.total, - text.research.year, - ].map((headerText, index) => ( - {headerText} - ))} - - - - - - - - - } - > - - - -
    -
    - -
    - }> + ({ + faculty: item.faculty?.person?.name || 'N/A', + department: item.faculty?.department?.name || 'N/A', + totalJobs: item.totalNoOfJobs, + total: item.totalAmount, + year: item.year, + }))} pageParamName="researchPage" + getCount={Promise.resolve([])} /> -
    +
    {/* PATENTS AND TECHNOLOGIES */} @@ -233,48 +181,33 @@ export default async function PatentsAndTechnology({ />
    -
    - - - - {[ - text.patentsAndTechnologies.number, - text.patentsAndTechnologies.applicationNumber, - text.patentsAndTechnologies.patentNumber, - text.patentsAndTechnologies.techTitle, - text.patentsAndTechnologies.inventor, - ].map((headerText, index) => ( - {headerText} - ))} - - - - - - - - - } - > - - - -
    -
    - -
    - }> + ({ + applicationNumber: item.applicationNumber, + patentNumber: item.patentNumber, + techTitle: item.title, + inventor: item.inventors, + }))} pageParamName="patentsPage" + getCount={Promise.resolve([])} /> -
    +
    {/* COPYRIGHTS AND DESIGNS */} @@ -293,79 +226,53 @@ export default async function PatentsAndTechnology({ {/* COPYRIGHTS TABLE */}
    -
    - - - - {[ - text.copyright.sNo, - text.copyright.grantYear, - text.copyright.copyrightNo, - text.copyright.title, - text.copyright.creator, - ].map((headerText, index) => ( - {headerText} - ))} - - - - }> - - - -
    -
    -
    - }> + ({ + grantYear: item.grantYear, + copyrightNo: item.copyrightNo, + title: item.title, + creator: item.creator, + }))} pageParamName="copyrightsPage" + getCount={Promise.resolve([])} /> -
    +

    {text.sections.copyright.design}

    {/* DESIGNS TABLE */}
    -
    - - - - {[ - text.design.sNo, - text.design.dateOfRegistration, - text.design.designNumber, - text.design.title, - text.design.creator, - text.design.class, - ].map((headerText, index) => ( - {headerText} - ))} - - - - }> - - - -
    -
    -
    - }> + ({ + dateOfRegistration: item.dateOfRegistration, + designNumber: item.designNumber, + title: item.title, + creator: item.creator, + class: item.class, + }))} pageParamName="designsPage" + getCount={Promise.resolve([])} /> -
    +
    @@ -379,46 +286,20 @@ export default async function PatentsAndTechnology({ />
    -
    - - - - {[ - text.memorandum.number, - text.memorandum.organization, - text.memorandum.signingDate, - ].map((headerText, index) => ( - {headerText} - ))} - - - - - - - - - } - > - - - -
    -
    - -
    - }> + ({ + organization: item.organization, + signingDate: item.date, + }))} pageParamName="memorandumPage" + getCount={Promise.resolve([])} /> -
    +
    {/* SPONSORED PROJECTS */} @@ -431,55 +312,37 @@ export default async function PatentsAndTechnology({ />
    -
    - - - - {[ - text.projects.number, - text.projects.year, - text.projects.department, - text.projects.facultyName, - text.projects.title, - text.projects.agency, - text.projects.amount, - text.projects.sanctionedFileOrderNo, - text.projects.sanctionedDate, - text.projects.status, - ].map((headerText, index) => ( - - {headerText} - - ))} - - - - - - - - - } - > - - - -
    -
    - -
    - }> + ({ + year: item.year, + department: item.department, + facultyName: item.facultyName, + title: item.title, + agency: item.agency, + amount: item.amount, + sanctionedFileOrderNo: item.sanctionedFileOrderNo, + sanctionedDate: item.sanctionedDate, + status: item.status, + }))} pageParamName="projectsPage" + getCount={Promise.resolve([])} /> -
    +
    {/* IMPORTANT RESOURCES */} @@ -527,250 +390,3 @@ export default async function PatentsAndTechnology({ ); } - -const PatentTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: PatentsTable[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, - item.applicationNumber, - item.patentNumber, - item.title, - item.inventors, - ]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; - -const CopyrightTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: CopyrightsTable[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, - item.grantYear, - item.copyrightNo, - item.title, - item.creator, - ]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; - -const DesignTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: DesignsTable[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, - item.dateOfRegistration, - item.designNumber, - item.title, - item.creator, - item.class, - ]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; - -const ResearchTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: (ResearchAndConsultancyTable & { - faculty: { - person: { name: string }; - department: { name: string }; - }; - })[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, - item.faculty?.person?.name || 'N/A', - item.faculty?.department?.name || 'N/A', - item.totalNoOfJobs, - item.totalAmount, - item.year, - ]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; - -const MemorandumTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: { - organization: string; - date: string; - }[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [startIndex + index + 1, item.organization, item.date]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; - -const ProjectsTable = ({ - tableData, - currentPage, - itemsPerPage = 10, -}: { - tableData: { - year: string; - department: string; - facultyName: string; - title: string; - agency: string; - amount: string; - sanctionedFileOrderNo: string; - sanctionedDate: string; - status: string; - }[]; - currentPage: number; - itemsPerPage?: number; -}) => { - const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - - return ( - <> - {visibleData.map((item, index) => { - const cellData = [ - startIndex + index + 1, - item.year, - item.department, - item.facultyName, - item.title, - item.agency, - item.amount, - item.sanctionedFileOrderNo, - item.sanctionedDate, - item.status, - ]; - - return ( - - {cellData.map((cellContent, cellIndex) => ( - {cellContent} - ))} - - ); - })} - - ); -}; diff --git a/components/ui/generic-table.tsx b/components/ui/generic-table.tsx index b724c017f..89fe7aa6e 100644 --- a/components/ui/generic-table.tsx +++ b/components/ui/generic-table.tsx @@ -3,6 +3,7 @@ import { Suspense } from 'react'; import Link from 'next/link'; import { FiExternalLink } from 'react-icons/fi'; +import { useSearchParams } from 'next/navigation'; import { Table, @@ -23,9 +24,10 @@ interface HeaderConfig { interface GenericTableProps> { headers: HeaderConfig[]; tableData: T[]; - currentPage: number; + currentPage?: number; itemsPerPage?: number; - getCount: Promise<{ count: number }[]>; + getCount?: Promise<{ count: number }[]>; + pageParamName?: string; } // Helper function to check if a value is a valid URL (absolute or relative) @@ -45,12 +47,17 @@ const isValidUrl = (value: unknown): boolean => { export default function GenericTable>({ headers, tableData, - currentPage, + currentPage: propCurrentPage, itemsPerPage = 10, + pageParamName = 'page', }: GenericTableProps) { + const searchParams = useSearchParams(); + const currentPage = + propCurrentPage ?? (Number(searchParams.get(pageParamName)) || 1); const startIndex = (currentPage - 1) * itemsPerPage; const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); const totalCount = tableData.length; + const noOfPages = Math.ceil(totalCount / itemsPerPage); return (
    @@ -108,12 +115,15 @@ export default function GenericTable>({ -
    - -
    + {noOfPages > 1 && ( +
    + +
    + )}
    ); } From 6311731b50a37f5b2fe8206d87ab2d7a8032bfdf Mon Sep 17 00:00:00 2001 From: Soumil Jain Date: Mon, 19 Jan 2026 04:23:08 +0530 Subject: [PATCH 19/73] Administration page Populated TABLES for : BOG ,Senate ,SCSA, Other officers, Financial and working committee, Building and construction commitee --- app/[locale]/events/page.tsx | 3 +- app/[locale]/header.tsx | 2 +- .../(committees)/board-of-governors/page.tsx | 131 +++++++++++- .../building-and-work-committee/page.tsx | 120 ++++++++++- .../(committees)/financial-committee/page.tsx | 118 ++++++++++- .../administration/(committees)/scsa/page.tsx | 107 ++++++++++ .../(committees)/senate/page.tsx | 112 ++++++++++- .../administration/director/page.tsx | 2 +- .../administration/other-officers/page.tsx | 189 +++++++++++++++++- .../institute/administration/page.tsx | 10 +- app/[locale]/notifications/SearchInput.tsx | 2 +- components/ui/generic-table.tsx | 102 +++++++++- i18n/en.ts | 41 +++- i18n/hi.ts | 41 +++- i18n/translations.ts | 17 +- .../board-of-governors-meetings.schema.ts | 23 +++ server/db/schema/board-of-governors.schema.ts | 7 + ...building-and-work-agenda-minutes.schema.ts | 24 +++ .../building-and-work-composition.schema.ts | 20 ++ server/db/schema/faculty.schema.ts | 2 +- .../financial-committee-meetings.schema.ts | 25 +++ .../db/schema/financial-committee.schema.ts | 7 + server/db/schema/index.ts | 12 ++ server/db/schema/other-officers.schema.ts | 49 +++++ server/db/schema/scsa_minutes.schema.ts | 18 ++ .../db/schema/senate-agenda-minutes.schema.ts | 24 +++ server/db/schema/senate-composition.schema.ts | 17 ++ 27 files changed, 1168 insertions(+), 57 deletions(-) create mode 100644 app/[locale]/institute/administration/(committees)/scsa/page.tsx create mode 100644 server/db/schema/board-of-governors-meetings.schema.ts create mode 100644 server/db/schema/board-of-governors.schema.ts create mode 100644 server/db/schema/building-and-work-agenda-minutes.schema.ts create mode 100644 server/db/schema/building-and-work-composition.schema.ts create mode 100644 server/db/schema/financial-committee-meetings.schema.ts create mode 100644 server/db/schema/financial-committee.schema.ts create mode 100644 server/db/schema/other-officers.schema.ts create mode 100644 server/db/schema/scsa_minutes.schema.ts create mode 100644 server/db/schema/senate-agenda-minutes.schema.ts create mode 100644 server/db/schema/senate-composition.schema.ts diff --git a/app/[locale]/events/page.tsx b/app/[locale]/events/page.tsx index 14cff99dc..53a322aa7 100644 --- a/app/[locale]/events/page.tsx +++ b/app/[locale]/events/page.tsx @@ -50,10 +50,9 @@ export default async function EventsPage({ ), orderBy: (e) => [desc(e.startDate)], limit: INITIAL_BATCH_SIZE + 1, // +1 to check if there are more - }); + }); console.log(raw); - // Category filter (multi) - check if event has ANY of the selected categories if (categories.length) { diff --git a/app/[locale]/header.tsx b/app/[locale]/header.tsx index 6e6094bff..be53c3d57 100644 --- a/app/[locale]/header.tsx +++ b/app/[locale]/header.tsx @@ -107,7 +107,7 @@ export default async function Header({ locale }: { locale: string }) { href: '/academics/scholarships', description: 'Learn about scholarships, eligibility, and application details.', - } + }, ], }, { label: text.faculty, href: 'faculty-and-staff' }, diff --git a/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx b/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx index 93131ef02..96f8c37be 100644 --- a/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx +++ b/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx @@ -1,16 +1,137 @@ -// Revalidate every hour (has DB calls via Committee, rarely changes) +// Revalidate every hour (has DB calls) export const revalidate = 3600; -import Committee from '../committee'; +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; + +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; export default async function BoardOfGovernors({ params: { locale }, - searchParams, }: { params: { locale: string }; - searchParams: { meetingPage?: string }; }) { + const text = (await getTranslations(locale)).Committee; + + // Fetch members from boardOfGovernors table + const members = await db.query.boardOfGovernors.findMany({ + orderBy: (member, { asc }) => [asc(member.id)], + }); + + // Fetch meetings from bogMeetings table + const meetings = await db.query.bogMeetings.findMany({ + orderBy: (meeting, { desc }) => [desc(meeting.id)], + }); + + const membersHeaders = [ + { key: 'name', label: text.members.name }, + { key: 'servedAs', label: text.members.servingAs }, + ]; + + const membersData = members.map((member) => ({ + name: member.name, + servedAs: member.servedAs, + })); + + const meetingsHeaders = [ + { key: 'meetingNo', label: text.meetings.serial }, + { key: 'date', label: text.meetings.date }, + { key: 'agenda', label: text.meetings.agenda }, + { key: 'minutes', label: text.meetings.minutes }, + ]; + + const formatDocumentLinks = ( + links: string[], + label: string, + meetingNo: string + ) => { + if (links.length === 0) return '-'; + + if (links.length === 1) { + return ( + + {label}_{meetingNo} + + + ); + } + + // Multiple parts: Agenda_52nd (Part 1, Part 2, Part 3) + return ( + + {label}_{meetingNo} ( + {links.map((link, index) => ( + + + Part {index + 1} + + + {index < links.length - 1 ? ', ' : ''} + + ))} + ) + + ); + }; + + const meetingsData = meetings.map((meeting) => ({ + meetingNo: meeting.meetingNo, + date: new Date(meeting.date).toLocaleDateString(locale, { + day: '2-digit', + month: 'long', + year: 'numeric', + }), + agenda: formatDocumentLinks(meeting.agenda, 'Agenda', meeting.meetingNo), + minutes: formatDocumentLinks(meeting.minutes, 'Minutes', meeting.meetingNo), + created_at: meeting.createdAt, + })); + return ( - + <> + +
    +
    + + +
    + +
    + + + +
    +
    + ); } diff --git a/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx b/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx index 6ecec28bd..f377e8288 100644 --- a/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx +++ b/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx @@ -1,16 +1,124 @@ -// Revalidate every hour (has DB calls via Committee, rarely changes) +// Revalidate every hour (has DB calls) export const revalidate = 3600; -import Committee from '../committee'; +import { Suspense } from 'react'; +import { asc, desc } from 'drizzle-orm'; -export default function BuildingAndWorkCommittee({ +import Loading from '~/app/loading'; +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; +import { + buildingAndWorkAgendaMinutes, + buildingAndWorkComposition, +} from '~/server/db/schema'; + +export default async function BuildingAndWorkPage({ params: { locale }, - searchParams, }: { params: { locale: string }; - searchParams: { meetingPage?: string }; + searchParams: { + compositionPage?: string; + agendaMinutesPage?: string; + }; }) { + const text = (await getTranslations(locale)).Committee; + + const members = await db + .select() + .from(buildingAndWorkComposition) + .orderBy(asc(buildingAndWorkComposition.id)); + + const meetings = await db + .select() + .from(buildingAndWorkAgendaMinutes) + .orderBy(desc(buildingAndWorkAgendaMinutes.id)); + + const membersHeaders = [ + { key: 'name', label: text.members.name }, + { key: 'servedAs', label: text.members.servingAs }, + ]; + + const membersData = members.map((member) => ({ + name: member.name.join(','), + servedAs: member.servedAs, + })); + + const meetingsHeaders = [ + { key: 'meetingNo', label: text.meetings.serial }, + { key: 'date', label: text.meetings.date }, + { key: 'agenda', label: text.meetings.agenda }, + { key: 'minutes', label: text.meetings.minutes }, + ]; + + const meetingsData = meetings.map((meeting) => ({ + meetingNo: meeting.meetingNo, + date: new Date(meeting.date).toLocaleDateString(locale, { + day: '2-digit', + month: 'long', + year: 'numeric', + }), + agenda: meeting.agenda?.[0] + ? { + url: meeting.agenda[0], + label: `${text.meetings.agendaOf} ${meeting.meetingNo} ${text.meetings.meeting}`, + } + : '-', + minutes: meeting.minutes?.[0] + ? { + url: meeting.minutes[0], + label: `${text.meetings.minutesOf} ${meeting.meetingNo} ${text.meetings.meeting}`, + } + : '-', + created_at: meeting.createdAt, + })); + return ( - + <> + + + {/* Table 1: Composition */} +
    + + +
    + }> + + +
    +
    + + {/* Table 2: Meeting Agenda and Minutes */} +
    + + +
    + +
    +
    + ); } diff --git a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx index a55bddbe4..ea7987adb 100644 --- a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx +++ b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx @@ -1,16 +1,122 @@ -// Revalidate every hour (has DB calls via Committee, rarely changes) +// Revalidate every hour (has DB calls) export const revalidate = 3600; -import Committee from '../committee'; +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; -export default function FinancialCommittee({ +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; + +export default async function FinancialCommittee({ params: { locale }, - searchParams, }: { params: { locale: string }; - searchParams: { meetingPage?: string }; }) { + const text = (await getTranslations(locale)).Committee; + + // Fetch members from financialCommittee table + const members = await db.query.financialCommittee.findMany({ + orderBy: (member, { asc }) => [asc(member.id)], + }); + + // Fetch meetings from financialCommitteeMeetings table + const meetings = await db.query.financialCommitteeMeetings.findMany({ + orderBy: (meeting, { desc }) => [desc(meeting.id)], + }); + + const membersHeaders = [ + { key: 'name', label: text.members.name }, + { key: 'servedAs', label: text.members.servingAs }, + ]; + + const membersData = members.map((member) => ({ + name: member.name, + servedAs: member.servedAs, + })); + + const meetingsHeaders = [ + { key: 'meetingNo', label: text.meetings.serial }, + { key: 'agenda', label: text.meetings.agenda }, + { key: 'minutes', label: text.meetings.minutes }, + ]; + + const formatDocumentLinks = ( + links: string[], + label: string, + meetingNo: string + ) => { + if (links.length === 0) return '-'; + + if (links.length === 1) { + return ( + + {label}_{meetingNo} + + + ); + } + + // Multiple parts: Agenda_52nd (Part 1, Part 2, Part 3) + return ( + + {label}_{meetingNo} ( + {links.map((link, index) => ( + + + Part {index + 1} + + + {index < links.length - 1 ? ', ' : ''} + + ))} + ) + + ); + }; + + const meetingsData = meetings.map((meeting) => ({ + meetingNo: meeting.meetingNo, + agenda: formatDocumentLinks(meeting.agenda, 'Agenda', meeting.meetingNo), + minutes: formatDocumentLinks(meeting.minutes, 'Minutes', meeting.meetingNo), + created_at: meeting.createdAt, + })); + return ( - + <> + +
    + + + + + +
    + ); } diff --git a/app/[locale]/institute/administration/(committees)/scsa/page.tsx b/app/[locale]/institute/administration/(committees)/scsa/page.tsx new file mode 100644 index 000000000..64442b84d --- /dev/null +++ b/app/[locale]/institute/administration/(committees)/scsa/page.tsx @@ -0,0 +1,107 @@ +// Revalidate every hour (has DB calls) +export const revalidate = 3600; + +import Link from 'next/link'; +import { FiExternalLink } from 'react-icons/fi'; + +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; + +export default async function SCSAPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const text = (await getTranslations(locale)).Committee; + + // Fetch meetings from scsa_minutes table + const meetings = await db.query.scsa_minutes.findMany({ + orderBy: (meeting, { desc }) => [desc(meeting.id)], + }); + + const meetingsHeaders = [ + { key: 'meetingNo', label: text.meetings.serial }, + { key: 'date', label: text.meetings.date }, + { key: 'minutes', label: text.meetings.minutes }, + ]; + + const formatDocumentLinks = ( + links: string[], + label: string, + meetingNo: string + ) => { + if (links.length === 0) return '-'; + + if (links.length === 1) { + return ( + + {label}_{meetingNo} + + + ); + } + + // Multiple parts: Minutes_52nd (Part 1, Part 2, Part 3) + return ( + + {label}_{meetingNo} ( + {links.map((link, index) => ( + + + Part {index + 1} + + + {index < links.length - 1 ? ', ' : ''} + + ))} + ) + + ); + }; + + const meetingsData = meetings.map((meeting) => ({ + meetingNo: meeting.meetingNo, + date: meeting.date + ? new Date(meeting.date).toLocaleDateString(locale, { + day: '2-digit', + month: 'long', + year: 'numeric', + }) + : '-', + minutes: formatDocumentLinks(meeting.minutes, 'Minutes', meeting.meetingNo), + created_at: meeting.createdAt, + })); + + return ( + <> + +
    + + +
    + + ); +} diff --git a/app/[locale]/institute/administration/(committees)/senate/page.tsx b/app/[locale]/institute/administration/(committees)/senate/page.tsx index 3359d6546..406038c57 100644 --- a/app/[locale]/institute/administration/(committees)/senate/page.tsx +++ b/app/[locale]/institute/administration/(committees)/senate/page.tsx @@ -1,21 +1,117 @@ -// Revalidate every hour (has DB calls via Committee, rarely changes) +// Revalidate every hour (has DB calls) export const revalidate = 3600; -import ImageHeader from '~/components/image-header'; +import { Suspense } from 'react'; -import Committee from '../committee'; +import Loading from '~/app/loading'; +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; -export default async function Senate({ +export default async function SenatePage({ params: { locale }, - searchParams, }: { params: { locale: string }; - searchParams: { meetingPage?: string }; + searchParams: { + meetingPage?: string; + compositionPage?: string; + agendaMinutesPage?: string; + }; }) { + const text = (await getTranslations(locale)).Committee; + const members = await db.query.senateComposition.findMany({ + orderBy: (member, { asc }) => [asc(member.id)], + }); + + const meetings = await db.query.senateAgendaMinutes.findMany({ + orderBy: (meeting, { desc }) => [desc(meeting.id)], + }); + + const membersHeaders = [ + { key: 'name', label: text.members.name }, + { key: 'servedAs', label: text.members.servingAs }, + ]; + + const membersData = members.map((member) => ({ + name: member.name.join(','), + servedAs: member.servedAs, + })); + + const meetingsHeaders = [ + { key: 'meetingNo', label: text.meetings.serial }, + { key: 'date', label: text.meetings.date }, + { key: 'agenda', label: text.meetings.agenda }, + { key: 'minutes', label: text.meetings.minutes }, + ]; + + const meetingsData = meetings.map((meeting) => ({ + meetingNo: meeting.meetingNo, + date: new Date(meeting.date).toLocaleDateString(locale, { + day: '2-digit', + month: 'long', + year: 'numeric', + }), + agenda: meeting.agenda?.[0] + ? { + url: meeting.agenda[0], + label: `${text.meetings.agendaOf} ${meeting.meetingNo} ${text.meetings.meeting}`, + } + : '-', + minutes: meeting.minutes?.[0] + ? { + url: meeting.minutes[0], + label: `${text.meetings.minutesOf} ${meeting.meetingNo} ${text.meetings.meeting}`, + } + : '-', + created_at: meeting.createdAt, + })); + return ( <> - - + + + {/* Table 1: Composition */} +
    + + +
    + }> + + +
    +
    + + {/* Table 2: Meeting Agenda and Minutes */} +
    + + +
    + +
    +
    ); } diff --git a/app/[locale]/institute/administration/director/page.tsx b/app/[locale]/institute/administration/director/page.tsx index 9e90a4e6b..22a0cc48b 100644 --- a/app/[locale]/institute/administration/director/page.tsx +++ b/app/[locale]/institute/administration/director/page.tsx @@ -156,4 +156,4 @@ export default async function DirectorCorner({ ); -} +} \ No newline at end of file diff --git a/app/[locale]/institute/administration/other-officers/page.tsx b/app/[locale]/institute/administration/other-officers/page.tsx index c52950619..1c6ce4968 100644 --- a/app/[locale]/institute/administration/other-officers/page.tsx +++ b/app/[locale]/institute/administration/other-officers/page.tsx @@ -1,9 +1,192 @@ -import { WorkInProgressStatus } from '~/components/status'; +// Revalidate every hour (has DB calls, rarely changes) +export const revalidate = 3600; -export default function Deans({ +import { Suspense } from 'react'; + +import Heading from '~/components/heading'; +import Loading from '~/components/loading'; +import ImageHeader from '~/components/image-header'; +import GenericTable from '~/components/ui/generic-table'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; + +async function fetchOfficersByCategory() { + const allOfficers = await db.query.otherOfficers.findMany({ + with: { + faculty: { + with: { + person: true, // Include person relation from faculty + }, + }, + }, + }); + + // Group by category + const grouped = allOfficers.reduce( + (acc, officer) => { + const category = officer.category; + if (!acc[category]) { + acc[category] = []; + } + acc[category].push({ + name: officer.faculty?.person?.name || 'N/A', + designation: officer.designation, + }); + return acc; + }, + {} as Record + ); + + return grouped; +} + +export default async function OfficersPage({ params: { locale }, }: { params: { locale: string }; }) { - return ; + const officersData = await fetchOfficersByCategory(); + const text = (await getTranslations(locale)).otherOfficersPage; + const OFFICER_CATEGORIES = [ + { + key: 'head-of-department', + title: text.categories[0], + id: 'head-of-department', + }, + { + key: 'chairman', + title: text.categories[1], + id: 'chairman', + }, + { + key: 'professor-in-charge', + title: text.categories[2], + id: 'professor-in-charge', + }, + { + key: 'faculty-in-charge', + title: text.categories[3], + id: 'faculty-in-charge', + }, + { + key: 'faculty-in-charge-student-club', + title: text.categories[4], + id: 'faculty-in-charge-student-club', + }, + { + key: 'members-library-committee', + title: text.categories[5], + id: 'members-library-committee', + }, + { + key: 'members-institute-handbook', + title: text.categories[6], + id: 'members-institute-handbook', + }, + { + key: 'members-sports-committee', + title: text.categories[7], + id: 'members-sports-committee', + }, + { + key: 'members-admission-committee', + title: text.categories[8], + id: 'members-admission-committee', + }, + { + key: 'members-grievance-cell', + title: text.categories[9], + id: 'members-grievance-cell', + }, + { + key: 'members-canteen-committee', + title: text.categories[10], + id: 'members-canteen-committee', + }, + { + key: 'members-clubs-committee', + title: text.categories[11], + id: 'members-clubs-committee', + }, + { + key: 'members-proctorial-board', + title: text.categories[12], + id: 'members-proctorial-board', + }, + { + key: 'members-examination-committee', + title: text.categories[13], + id: 'members-examination-committee', + }, + { + key: 'members-disciplinary-committee', + title: text.categories[14], + id: 'members-disciplinary-committee', + }, + { + key: 'members-anti-ragging-committee', + title: text.categories[15], + id: 'members-anti-ragging-committee', + }, + { + key: 'members-nirf-nba-naac', + title: text.categories[16], + id: 'members-nirf-nba-naac', + }, + { + key: 'coordinator', + title: text.categories[17], + id: 'coordinator', + }, + { + key: 'co-coordinator', + title: text.categories[18], + id: 'co-coordinator', + }, + { + key: 'nodal-officer', + title: text.categories[19], + id: 'nodal-officer', + }, + ] as const; + return ( + <> + + {OFFICER_CATEGORIES.map((category) => { + const categoryData = officersData[category.key] || []; + + // Skip rendering if no data for this category + if (categoryData.length === 0) return null; + + return ( +
    + +
    + }> + ({ + name: item.name, + designation: item.designation || 'N/A', + }))} + pageParamName={`${category.key}Page`} + getCount={Promise.resolve([])} + serialNoLabel={text.serialNo} + /> + +
    +
    + ); + })} + + ); } diff --git a/app/[locale]/institute/administration/page.tsx b/app/[locale]/institute/administration/page.tsx index acb0a4bdb..688648c9e 100644 --- a/app/[locale]/institute/administration/page.tsx +++ b/app/[locale]/institute/administration/page.tsx @@ -104,18 +104,18 @@ export default async function Administration({ { + return ( + typeof value === 'object' && + value !== null && + 'url' in value && + 'label' in value && + typeof (value as LabeledLink).url === 'string' && + typeof (value as LabeledLink).label === 'string' + ); +}; + +type SortOrder = 'asc' | 'desc'; + interface GenericTableProps> { headers: HeaderConfig[]; tableData: T[]; @@ -28,6 +48,13 @@ interface GenericTableProps> { itemsPerPage?: number; getCount?: Promise<{ count: number }[]>; pageParamName?: string; + showSerialNo?: boolean; + /** Label for the serial number column. Defaults to 'No.' */ + serialNoLabel?: string; + /** Enable sorting by a date field. Pass the key of the date field to sort by (e.g., 'created_at', 'date') */ + sortByDateField?: keyof T; + /** Default sort order when sorting is enabled. Defaults to 'desc' */ + defaultSortOrder?: SortOrder; } // Helper function to check if a value is a valid URL (absolute or relative) @@ -50,24 +77,69 @@ export default function GenericTable>({ currentPage: propCurrentPage, itemsPerPage = 10, pageParamName = 'page', + showSerialNo = true, + serialNoLabel = 'No.', + sortByDateField, + defaultSortOrder = 'desc', }: GenericTableProps) { const searchParams = useSearchParams(); + const [sortOrder, setSortOrder] = useState(defaultSortOrder); + + const sortedData = useMemo(() => { + if (!sortByDateField) return tableData; + + return [...tableData].sort((a, b) => { + const aValue = a[sortByDateField]; + const bValue = b[sortByDateField]; + + // Handle Date objects, date strings, or timestamps + const aDate = aValue instanceof Date ? aValue : new Date(String(aValue)); + const bDate = bValue instanceof Date ? bValue : new Date(String(bValue)); + + if (sortOrder === 'asc') { + return aDate.getTime() - bDate.getTime(); + } + return bDate.getTime() - aDate.getTime(); + }); + }, [tableData, sortByDateField, sortOrder]); + const currentPage = propCurrentPage ?? (Number(searchParams.get(pageParamName)) || 1); const startIndex = (currentPage - 1) * itemsPerPage; - const visibleData = tableData.slice(startIndex, startIndex + itemsPerPage); - const totalCount = tableData.length; + const visibleData = sortedData.slice(startIndex, startIndex + itemsPerPage); + const totalCount = sortedData.length; const noOfPages = Math.ceil(totalCount / itemsPerPage); + const toggleSortOrder = () => { + setSortOrder((prev) => (prev === 'asc' ? 'desc' : 'asc')); + }; + return (
    - No. + {showSerialNo && {serialNoLabel}} {headers.map((header, index) => ( - {header.label} + + {index === 0 && sortByDateField ? ( + + ) : ( + header.label + )} + ))} @@ -76,7 +148,7 @@ export default function GenericTable>({ - + @@ -87,15 +159,29 @@ export default function GenericTable>({ key={rowIndex} className="text-neutral-700 hover:bg-neutral-50" > - {startIndex + rowIndex + 1} + {showSerialNo && ( + {startIndex + rowIndex + 1} + )} {headers.map((header, colIndex) => { const cellValue = item[header.key]; + const labeledLink = isLabeledLink(cellValue); const isLink = isValidUrl(cellValue); return ( - {isLink ? ( + {isValidElement(cellValue) ? ( + cellValue + ) : labeledLink ? ( + + {cellValue.label}{' '} + + + ) : isLink ? ( ({ + id: t.serial('id').primaryKey(), + meetingNo: t.varchar('meeting_no', { length: 16 }).notNull(), + date: t.date('date').notNull(), + agenda: t + .text('agenda') + .array() + .notNull() + .default(sql`'{}'::text[]`), + minutes: t + .text('minutes') + .array() + .notNull() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), +})); \ No newline at end of file diff --git a/server/db/schema/board-of-governors.schema.ts b/server/db/schema/board-of-governors.schema.ts new file mode 100644 index 000000000..2dc670dd4 --- /dev/null +++ b/server/db/schema/board-of-governors.schema.ts @@ -0,0 +1,7 @@ +import { pgTable } from 'drizzle-orm/pg-core'; + +export const boardOfGovernors = pgTable('board_of_governors', (t) => ({ + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + servedAs: t.varchar('served_as', { length: 64 }).notNull(), +})); diff --git a/server/db/schema/building-and-work-agenda-minutes.schema.ts b/server/db/schema/building-and-work-agenda-minutes.schema.ts new file mode 100644 index 000000000..5d08ab570 --- /dev/null +++ b/server/db/schema/building-and-work-agenda-minutes.schema.ts @@ -0,0 +1,24 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const buildingAndWorkAgendaMinutes = pgTable( + 'building_and_work_agenda_and_minutes', + (t) => ({ + id: t.serial('id').primaryKey(), + meetingNo: t.varchar('meeting_no', { length: 16 }).notNull(), + date: t.date('date').notNull(), + agenda: t + .text('agenda') + .array() + .default(sql`'{}'::text[]`), + minutes: t + .text('minutes') + .array() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), + }) +); diff --git a/server/db/schema/building-and-work-composition.schema.ts b/server/db/schema/building-and-work-composition.schema.ts new file mode 100644 index 000000000..8c170dee5 --- /dev/null +++ b/server/db/schema/building-and-work-composition.schema.ts @@ -0,0 +1,20 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const buildingAndWorkComposition = pgTable( + 'building_and_work_composition', + (t) => ({ + id: t.serial('id').primaryKey(), + name: t + .text() + .array() + .notNull() + .default(sql`'{}'::text[]`), + servedAs: t.varchar('served_as', { length: 256 }).notNull(), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), + }) +); diff --git a/server/db/schema/faculty.schema.ts b/server/db/schema/faculty.schema.ts index af442bd87..202f4abcc 100644 --- a/server/db/schema/faculty.schema.ts +++ b/server/db/schema/faculty.schema.ts @@ -44,7 +44,7 @@ export const faculty = pgTable( researchGateId: t.text(), scopusId: t.text(), areasOfInterest: t.text().array().default([]), - }), + }) // (table) => [uniqueIndex('faculty_employee_id_idx').on(table.employeeId)] ); diff --git a/server/db/schema/financial-committee-meetings.schema.ts b/server/db/schema/financial-committee-meetings.schema.ts new file mode 100644 index 000000000..b9d8a48fd --- /dev/null +++ b/server/db/schema/financial-committee-meetings.schema.ts @@ -0,0 +1,25 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const financialCommitteeMeetings = pgTable( + 'financial_committee_meetings', + (t) => ({ + id: t.serial('id').primaryKey(), + meetingNo: t.varchar('meeting_no', { length: 16 }).notNull(), + agenda: t + .text('agenda') + .array() + .notNull() + .default(sql`'{}'::text[]`), + minutes: t + .text('minutes') + .array() + .notNull() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), + }) +); diff --git a/server/db/schema/financial-committee.schema.ts b/server/db/schema/financial-committee.schema.ts new file mode 100644 index 000000000..c054a9a1d --- /dev/null +++ b/server/db/schema/financial-committee.schema.ts @@ -0,0 +1,7 @@ +import { pgTable } from 'drizzle-orm/pg-core'; + +export const financialCommittee = pgTable('financial_committee', (t) => ({ + id: t.serial('id').primaryKey(), + name: t.text('name').notNull(), + servedAs: t.varchar('served_as', { length: 64 }).notNull(), +})); diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index 07a5786e9..78cfb59f2 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -1,3 +1,11 @@ +import exp from 'constants'; + +export * from './board-of-governors.schema'; +export * from './board-of-governors-meetings.schema'; +export * from './building-and-work-agenda-minutes.schema'; +export * from './building-and-work-composition.schema'; +export * from './financial-committee.schema'; +export * from './financial-committee-meetings.schema'; export * from './club-members.schema'; export * from './club-socials.schema'; export * from './copyrights.schema'; @@ -35,3 +43,7 @@ export * from './students.schema'; export * from './sponsored-research-projects.schema'; export * from './sponsored-research-projects-faculties.schema'; export * from './memorandum.schema'; +export * from './other-officers.schema'; +export * from './senate-composition.schema'; +export * from './senate-agenda-minutes.schema'; +export * from './scsa_minutes.schema'; diff --git a/server/db/schema/other-officers.schema.ts b/server/db/schema/other-officers.schema.ts new file mode 100644 index 000000000..eed9e3282 --- /dev/null +++ b/server/db/schema/other-officers.schema.ts @@ -0,0 +1,49 @@ +import { pgEnum, pgTable } from 'drizzle-orm/pg-core'; +import { relations } from 'drizzle-orm'; + +import { faculty } from './faculty.schema'; + +export const officerCategoryEnum = pgEnum('officer_category', [ + 'head-of-department', + 'chairman', + 'professor-in-charge', + 'faculty-in-charge', + 'faculty-in-charge-student-club', + 'members-library-committee', + 'members-institute-handbook', + 'members-sports-committee', + 'members-admission-committee', + 'members-grievance-cell', + 'members-canteen-committee', + 'members-clubs-committee', + 'members-proctorial-board', + 'members-examination-committee', + 'members-disciplinary-committee', + 'members-anti-ragging-committee', + 'members-nirf-nba-naac', + 'coordinator', + 'co-coordinator', + 'nodal-officer', +]); + +export const otherOfficers = pgTable('other-officers', (t) => ({ + id: t.serial('id').primaryKey(), + designation: t.varchar('designation', { length: 256 }).notNull(), + facultyId: t + .integer('faculty_id') + .notNull() + .references(() => faculty.id), + category: officerCategoryEnum('category').notNull(), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), +})); + +export const officersRelations = relations(otherOfficers, ({ one }) => ({ + faculty: one(faculty, { + fields: [otherOfficers.facultyId], + references: [faculty.id], + }), +})); diff --git a/server/db/schema/scsa_minutes.schema.ts b/server/db/schema/scsa_minutes.schema.ts new file mode 100644 index 000000000..b9facf6db --- /dev/null +++ b/server/db/schema/scsa_minutes.schema.ts @@ -0,0 +1,18 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const scsa_minutes = pgTable('scsa_minutes', (t) => ({ + id: t.serial('id').primaryKey(), + meetingNo: t.varchar('meeting_no', { length: 16 }).notNull().unique(), + date: t.date('date'), + minutes: t + .text('minutes') + .array() + .notNull() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), +})); diff --git a/server/db/schema/senate-agenda-minutes.schema.ts b/server/db/schema/senate-agenda-minutes.schema.ts new file mode 100644 index 000000000..228f0965d --- /dev/null +++ b/server/db/schema/senate-agenda-minutes.schema.ts @@ -0,0 +1,24 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const senateAgendaMinutes = pgTable( + 'senate_agenda_and_minutes', + (t) => ({ + id: t.serial('id').primaryKey(), + meetingNo: t.varchar('meeting_no', { length: 16 }).notNull(), + date: t.date('date').notNull(), + agenda: t + .text('agenda') + .array() + .default(sql`'{}'::text[]`), + minutes: t + .text('minutes') + .array() + .default(sql`'{}'::text[]`), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), + }) +); diff --git a/server/db/schema/senate-composition.schema.ts b/server/db/schema/senate-composition.schema.ts new file mode 100644 index 000000000..00589edf2 --- /dev/null +++ b/server/db/schema/senate-composition.schema.ts @@ -0,0 +1,17 @@ +import { pgTable } from 'drizzle-orm/pg-core'; +import { sql } from 'drizzle-orm'; + +export const senateComposition = pgTable('senate_composition', (t) => ({ + id: t.serial('id').primaryKey(), + name: t + .text() + .array() + .notNull() + .default(sql`'{}'::text[]`), + servedAs: t.varchar('served_as', { length: 256 }).notNull(), + createdAt: t.timestamp('created_at').defaultNow().notNull(), + updatedAt: t + .timestamp('updated_at') + .$onUpdate(() => new Date()) + .notNull(), +})); From 82723d4187f2ce5edbdf1bc958270474e28ffaed Mon Sep 17 00:00:00 2001 From: Navneet Kaur Date: Tue, 20 Jan 2026 01:49:22 +0530 Subject: [PATCH 20/73] fix: update sorting logic for meeting tables (#481) This pull request updates the sorting logic and meeting data handling across several committee pages to ensure meetings are sorted by the correct date or meeting number fields. It also improves the `GenericTable` component to handle numeric sorting properly. The main changes are grouped below: **Committee Pages: Sorting Field Updates** * Updated the `sortByDateField` prop in multiple committee pages (`board-of-governors`, `building-and-work-committee`, `scsa`, `senate`) to use the `date` field instead of `created_at` for more accurate meeting sorting. (`app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx` [app/[locale]/institute/administration/(committees)/board-of-governors/page.tsxL120-R131](diffhunk://#diff-c3c09c0b78573302e9114f7b5c8b3dd7b6595ad633647910670f7f0c0f69ff06L120-R131), `building-and-work-committee/page.tsx` [app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsxL118-R118](diffhunk://#diff-5103d70f013e00bb7173d6fc6151208540d89110a2c32feea4da0de2ca291880L118-R118), `scsa/page.tsx` [app/[locale]/institute/administration/(committees)/scsa/page.tsxL101-R101](diffhunk://#diff-48b94a66fa30b997acc051bf15874af155437d3aeac97a450bef0323d512c06cL101-R101), `senate/page.tsx` [app/[locale]/institute/administration/(committees)/senate/page.tsxL111-R111](diffhunk://#diff-8e004bb516d101f7280c73ab50955ab9c59f0a4f01063a82da819a8cd3d1666cL111-R111)) * For the `financial-committee` page, added a `meetingNumber` field to each meeting and updated sorting to use this numeric field for correct ordering. (`financial-committee/page.tsx` [app/[locale]/institute/administration/(committees)/financial-committee/page.tsxR90](diffhunk://#diff-b66c6cc2b15973120500a43f5cca35a648d7cb402671229bd74f46db9a10b307R90), [app/[locale]/institute/administration/(committees)/financial-committee/page.tsxL117-R118](diffhunk://#diff-b66c6cc2b15973120500a43f5cca35a648d7cb402671229bd74f46db9a10b307L117-R118)) **GenericTable Component: Sorting Enhancement** * Enhanced the `GenericTable` component to properly handle numeric sorting when the sort field is a number, ensuring correct order for fields like `meetingNumber`. (`components/ui/generic-table.tsx` [components/ui/generic-table.tsxR95-R99](diffhunk://#diff-1133b034223eb2492ab7f0ff0089616e410fd52886fa9704e4e613fcd55a804fR95-R99)) **Minor UI Improvements** * Removed unnecessary `id` attributes and adjusted the placement of the `agenda` section heading for improved accessibility and structure. (`board-of-governors/page.tsx` [app/[locale]/institute/administration/(committees)/board-of-governors/page.tsxL105-R105](diffhunk://#diff-c3c09c0b78573302e9114f7b5c8b3dd7b6595ad633647910670f7f0c0f69ff06L105-R105), [app/[locale]/institute/administration/(committees)/board-of-governors/page.tsxL120-R131](diffhunk://#diff-c3c09c0b78573302e9114f7b5c8b3dd7b6595ad633647910670f7f0c0f69ff06L120-R131)) --------- Co-authored-by: Aryawart-kathpal --- .../(committees)/board-of-governors/page.tsx | 6 +-- .../building-and-work-committee/page.tsx | 2 +- .../(committees)/financial-committee/page.tsx | 3 +- .../administration/(committees)/scsa/page.tsx | 2 +- .../(committees)/senate/page.tsx | 2 +- .../institute/administration/page.tsx | 47 +++++++++---------- components/ui/generic-table.tsx | 5 ++ 7 files changed, 34 insertions(+), 33 deletions(-) diff --git a/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx b/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx index 96f8c37be..621d09c1e 100644 --- a/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx +++ b/app/[locale]/institute/administration/(committees)/board-of-governors/page.tsx @@ -102,7 +102,7 @@ export default async function BoardOfGovernors({ <>
    -
    +
    -
    diff --git a/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx b/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx index f377e8288..a369b14fd 100644 --- a/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx +++ b/app/[locale]/institute/administration/(committees)/building-and-work-committee/page.tsx @@ -115,7 +115,7 @@ export default async function BuildingAndWorkPage({ tableData={meetingsData} pageParamName="agendaMinutesPage" showSerialNo={false} - sortByDateField="created_at" + sortByDateField="date" /> diff --git a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx index ea7987adb..f39c50395 100644 --- a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx +++ b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx @@ -87,6 +87,7 @@ export default async function FinancialCommittee({ const meetingsData = meetings.map((meeting) => ({ meetingNo: meeting.meetingNo, + meetingNumber: parseInt(meeting.meetingNo.replace(/\D/g, ''), 10) || 0, agenda: formatDocumentLinks(meeting.agenda, 'Agenda', meeting.meetingNo), minutes: formatDocumentLinks(meeting.minutes, 'Minutes', meeting.meetingNo), created_at: meeting.createdAt, @@ -114,7 +115,7 @@ export default async function FinancialCommittee({ tableData={meetingsData} pageParamName="meetingPage" showSerialNo={false} - sortByDateField="created_at" + sortByDateField="meetingNumber" /> diff --git a/app/[locale]/institute/administration/(committees)/scsa/page.tsx b/app/[locale]/institute/administration/(committees)/scsa/page.tsx index 64442b84d..121615ad4 100644 --- a/app/[locale]/institute/administration/(committees)/scsa/page.tsx +++ b/app/[locale]/institute/administration/(committees)/scsa/page.tsx @@ -98,7 +98,7 @@ export default async function SCSAPage({ tableData={meetingsData} pageParamName="meetingPage" showSerialNo={false} - sortByDateField="created_at" + sortByDateField="date" serialNoLabel={text.members.serial} /> diff --git a/app/[locale]/institute/administration/(committees)/senate/page.tsx b/app/[locale]/institute/administration/(committees)/senate/page.tsx index 406038c57..a03dfc598 100644 --- a/app/[locale]/institute/administration/(committees)/senate/page.tsx +++ b/app/[locale]/institute/administration/(committees)/senate/page.tsx @@ -108,7 +108,7 @@ export default async function SenatePage({ tableData={meetingsData} pageParamName="agendaMinutesPage" showSerialNo={false} - sortByDateField="created_at" + sortByDateField="date" /> diff --git a/app/[locale]/institute/administration/page.tsx b/app/[locale]/institute/administration/page.tsx index 688648c9e..3d5fc682e 100644 --- a/app/[locale]/institute/administration/page.tsx +++ b/app/[locale]/institute/administration/page.tsx @@ -57,11 +57,8 @@ export default async function Administration({ />
    -

    - {text.description} -

    - }> - - {text.composition} - - + }>
    + {/* DEANS */} + +
    + }> + + +
    + + - - -
    - }> - - -
    ); diff --git a/components/ui/generic-table.tsx b/components/ui/generic-table.tsx index 31725a40f..a9b314e27 100644 --- a/components/ui/generic-table.tsx +++ b/components/ui/generic-table.tsx @@ -92,6 +92,11 @@ export default function GenericTable>({ const aValue = a[sortByDateField]; const bValue = b[sortByDateField]; + // Handle numeric values + if (typeof aValue === 'number' && typeof bValue === 'number') { + return sortOrder === 'asc' ? aValue - bValue : bValue - aValue; + } + // Handle Date objects, date strings, or timestamps const aDate = aValue instanceof Date ? aValue : new Date(String(aValue)); const bDate = bValue instanceof Date ? bValue : new Date(String(bValue)); From ae5147ea3498e845ace77f891f8101b67c16e23c Mon Sep 17 00:00:00 2001 From: Rahul Gupta Date: Tue, 20 Jan 2026 04:10:50 +0530 Subject: [PATCH 21/73] adding scoe page (#479) adding scoe -page --------- Co-authored-by: Aryawart-kathpal Co-authored-by: ArnavSharma005 --- .../administration/director/page.tsx | 14 +- app/[locale]/scoe/page.tsx | 263 ++++++++++++++++++ components/fic-group.tsx | 102 +++++++ .../notifications/notifications-panel.tsx | 2 +- i18n/en.ts | 108 +++++++ i18n/hi.ts | 118 ++++++++ i18n/translations.ts | 67 +++++ 7 files changed, 666 insertions(+), 8 deletions(-) create mode 100644 app/[locale]/scoe/page.tsx create mode 100644 components/fic-group.tsx diff --git a/app/[locale]/institute/administration/director/page.tsx b/app/[locale]/institute/administration/director/page.tsx index 22a0cc48b..d00f36323 100644 --- a/app/[locale]/institute/administration/director/page.tsx +++ b/app/[locale]/institute/administration/director/page.tsx @@ -34,7 +34,7 @@ export default async function DirectorCorner({
    @@ -54,7 +54,7 @@ export default async function DirectorCorner({
    @@ -69,7 +69,7 @@ export default async function DirectorCorner({
    @@ -82,7 +82,7 @@ export default async function DirectorCorner({
    @@ -101,9 +101,9 @@ export default async function DirectorCorner({ />
    -

    +

    {employe.name} -

    + {employe.position} @@ -135,7 +135,7 @@ export default async function DirectorCorner({
    diff --git a/app/[locale]/scoe/page.tsx b/app/[locale]/scoe/page.tsx new file mode 100644 index 000000000..ef454aecf --- /dev/null +++ b/app/[locale]/scoe/page.tsx @@ -0,0 +1,263 @@ +import Image from 'next/image'; +import { MdCall, MdEmail } from 'react-icons/md'; + +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import { getTranslations } from '~/i18n/translations'; +import GenericTable from '~/components/ui/generic-table'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; +import FICGroup from '~/components/fic-group'; +import { getS3Url } from '~/server/s3'; +export default async function SCoE({ + params: { locale }, + searchParams, +}: { + params: { locale: string }; + searchParams?: { + labsPage?: string; + coursesPage?: string; + }; +}) { + const text = (await getTranslations(locale)).SCoE; + const base = getS3Url(); + + const CoursesData = text.Courses.list.map((course) => ({ + courseName: course, + })); + + const LaboratoriesData = text.Laboratories.list.map((lab) => ({ + LaboratoriesName: lab, + })); + + return ( + <> + + {/* ADMISSION */} +
    +
    + {text.admission.process.content.map((paragraph, index) => ( +

    + {paragraph} +

    + ))} +
    +
    + {/* notifications */} +
    + + +
    +
    +
    + {/* LEFT: Vision & Mission */} +
    + {/* Vision */} +
    +

    {text.Vision.title.toUpperCase()}

    +

    {text.Vision.description}

    +
    + + {/* Mission */} +
    +

    {text.Mission.title.toUpperCase()}

    +
      + {text.Mission.points.map((point, idx) => ( +
    • {point}
    • + ))} +
    +
    +
    + + {/* RIGHT: Image */} +
    + +
    +
    +
    +
    + + +
    + {/* features */} +
    + +
    +
    +
    +
    +
      + {text.Features.items.map((item, index) => ( +
    • + + + {item} + +
    • + ))} +
    +
    +
    +
    +
    +
    + \{/* laboratories */} +
    + +
    + + {/* Courses */} +
    + +
    + + {/* how to apply */} +
    + +
    +
    +
    +
    +
      + {text.How_to_Apply.registrationSteps.map((item, index) => ( +
    1. {item}
    2. + ))} +
    +
    +
    +
    +
    +
    + {/* for queries */} +
    + + +
    +
    + {/* Email */} +
    + + + + + scoe@nitkkr.ac.in + +
    + + {/* Phone */} +
    + + + + + 01744-233300 + +
    +
    +
    +
    + + ); +} diff --git a/components/fic-group.tsx b/components/fic-group.tsx new file mode 100644 index 000000000..80d5f499a --- /dev/null +++ b/components/fic-group.tsx @@ -0,0 +1,102 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { eq, inArray } from 'drizzle-orm'; +import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; + +import { db } from '~/server/db'; +import { faculty } from '~/server/db/schema'; + +interface FICGroupProps { + facultyData: { + employeeId: string; + designation: string; + }[]; +} + +export default async function FICGroup({ facultyData }: FICGroupProps) { + // Extract employee IDs from the input + const employeeIds = facultyData.map((f) => f.employeeId); + + // Fetch faculty members from database + const facultyMembers = await db.query.faculty.findMany({ + where: inArray(faculty.employeeId, employeeIds), + columns: { + employeeId: true, + }, + with: { + person: { + columns: { + name: true, + email: true, + telephone: true, + countryCode: true, // include imageUrl later when added.. currently keep fallback + }, + }, + }, + }); + + // Create a map for quick lookup of designations + const designationMap = new Map( + facultyData.map((f) => [f.employeeId, f.designation]) + ); + + // Combine faculty data with designations + const enrichedFaculty = facultyMembers.map((member) => ({ + ...member, + displayDesignation: designationMap.get(member.employeeId) ?? '', + })); + + return ( +
      + {enrichedFaculty.map((member) => ( +
    • + +
      +
      +

      + {member.person.name} +

      + + {member.displayDesignation} + +
      +
      + + + + {member.person.email} + + + + + + {member.person.countryCode + ? `${member.person.countryCode} ${member.person.telephone}` + : member.person.telephone} + + +
      +
      +
    • + ))} +
    + ); +} diff --git a/components/notifications/notifications-panel.tsx b/components/notifications/notifications-panel.tsx index 0561c4fb9..017277b76 100644 --- a/components/notifications/notifications-panel.tsx +++ b/components/notifications/notifications-panel.tsx @@ -66,7 +66,7 @@ export default async function NotificationsPanel({ return (
    Date: Wed, 21 Jan 2026 00:25:36 +0530 Subject: [PATCH 22/73] Fix excessive spacing in Events section on landing page (#484) ### What was changed - Reduced excessive vertical spacing in the Events section - Tightened spacing between filters and "View All" on mobile --- app/[locale]/events.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/[locale]/events.tsx b/app/[locale]/events.tsx index 30d8a9d08..9ee54e2af 100644 --- a/app/[locale]/events.tsx +++ b/app/[locale]/events.tsx @@ -53,7 +53,7 @@ export default async function Events({ return (
    ))} -
    +
    + + + + {text.researchProposalForms.table.srno} + + {text.researchProposalForms.table.form} + + + + {researchProposalForms.map((form, index) => ( + + {index + 1} + + + {text.researchProposalForms.formNames[index]} → + + + + ))} + +
    +
    +
    + {/* PARTNER INSTITUTES */} +
    + + +
    + + + + + {text.partnerInstitutes.table.srNo} + + + {text.partnerInstitutes.table.institute} + + + + + {text.partnerInstitutes.institutes.map((inst, index) => ( + + {index + 1} + {inst.name} + + ))} + +
    +
    +
    + {/* RESEARCH AREAS */} +
    + + +
    + {/* Left – Image card */} +
    + {text.researchAreas.heading} + + {/* Dark fade like your design */} +
    + + {/* Read more overlay */} + + {text.researchAreas.readMore} → + +
    + + {/* Right – Description */} +

    + {text.researchAreas.description} +

    +
    +
    + + + {/* FOR QUERIES */} +
    + + +
    +
    + {/* Email */} +
    + + + +
    +
    +
    +
    + + ); +} diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index 0c03cf2e2..3d97435b1 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -1,9 +1,18 @@ import Image from 'next/image'; import { BsLinkedin } from 'react-icons/bs'; import { MdEmail, MdPhone } from 'react-icons/md'; +import { + TbBuildingSkyscraper, + TbContract, + TbRocket, + TbSchool, +} from 'react-icons/tb'; import Notifications, { type NotificationCategory } from '~/app/notifications'; +import { getTranslations } from '~/i18n/translations'; +import { type eventCategoryEnum } from '~/server/db'; import { Button } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; import { AutoplayCarousel, CarouselContent, @@ -13,8 +22,6 @@ import { } from '~/components/carousels'; import Heading from '~/components/heading'; import MessageCard from '~/components/message-card'; -import { getTranslations } from '~/i18n/translations'; -import { type eventCategoryEnum } from '~/server/db'; import Events from './events'; @@ -131,6 +138,31 @@ export default async function Home({ }} /> + + ); } diff --git a/app/[locale]/scoe/page.tsx b/app/[locale]/scoe/page.tsx index ef454aecf..8cfbf6b6d 100644 --- a/app/[locale]/scoe/page.tsx +++ b/app/[locale]/scoe/page.tsx @@ -10,7 +10,6 @@ import FICGroup from '~/components/fic-group'; import { getS3Url } from '~/server/s3'; export default async function SCoE({ params: { locale }, - searchParams, }: { params: { locale: string }; searchParams?: { @@ -226,7 +225,7 @@ export default async function SCoE({ text={text.For_Queries.title.toUpperCase()} /> -
    +
    {/* Email */}
    @@ -237,9 +236,6 @@ export default async function SCoE({ > - - scoe@nitkkr.ac.in -
    {/* Phone */} @@ -251,9 +247,6 @@ export default async function SCoE({ > - - 01744-233300 -
    diff --git a/i18n/en.ts b/i18n/en.ts index b1ba60d79..3a8345f72 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -186,6 +186,12 @@ const text: Translations = { timeTable: 'Time Table', }, viewMore: 'View More', + buttons: { + hostels: 'Hostels', + racs: 'RAC-S (ISRO)', + scoe: 'CoE (Siemens)', + tenders: 'Tenders', + }, }, Academics: { notifications: 'Notifications', @@ -414,6 +420,86 @@ const text: Translations = { login: 'Login', profile: { alt: 'Profile image', view: 'View Profile' }, }, + + RACS: { + title: 'Regional Academic Centre for Space (RAC-S)', + intro: + 'Having recognized the imperative need to pursue advanced research in the areas of relevance to the future technological and programmatic needs of the Indian Space Programme, a Regional Academic Centre for Space (RAC-S) has been established at the Institute as a joint collaborative initiative of Indian Space Research Organization (ISRO) and NIT Kurukshetra. The Centre aims to act as a facilitator for the promotion of Space Technology related activities in the northern region of the country and to become an ambassador for the capacity building, awareness creation and R & D activities of ISRO.', + notificationsCategory: 'Notifications', + + // Tabs/Navigation + tabs: { + notifications: 'Notifications', + regionalCoordinator: 'Regional Coordinator', + researchProposalForms: 'Research Proposal Forms', + partnerInstitutes: 'Partner Institutes', + researchAreas: 'Research Areas', + queries: 'For Queries', + }, + + notifications: { + title: 'NOTIFICATIONS', + }, + + // Regional Coordinator Section + coordinator: { + heading: 'REGIONAL COORDINATOR', + name: 'Prof. Arun Goel', + position: 'Professor & Head, Regional Academic Centre for Space (RAC-S)', + email: 'drarun_goel@yahoo.co.in', + phone: '+91-1744-233XXX', + image: 'fallback/user-image.jpg', + }, + + // Research Proposal Forms Section + researchProposalForms: { + heading: 'Research Proposal Forms', + + table: { + srno: 'Sr. No.', + form: 'Form Name', + }, + + formNames: [ + 'Application for Grant of Funds', + 'Terms and Conditions of ISRO Research Grants', + 'Bio-data of the Investigator(s)', + 'Research Proposal (Form B)', + 'Research Areas of SAC March 2023', + ], + }, + + // Partner Institutes Section + partnerInstitutes: { + heading: 'PARTNER INSTITUTES', + table: { + srNo: 'Sr. No.', + institute: 'Institute Name', + }, + institutes: [ + { name: ' NIT Delhi' }, + { name: ' NIT Uttrakhand' }, + { name: 'Dr. B.R Ambedkar National Institutes of Technology Jalandar' }, + { name: 'NIT Srinagar (J&K)' }, + { name: 'Kurukshetra University Kurukshetra' }, + ], + }, + + // Research Areas Section + researchAreas: { + heading: 'RESEARCH AREAS', + description: + 'Indian Space Research Organisation (ISRO) plays a vital role in advancing space research and technology for national development. Established in 1969, ISRO has achieved global recognition through cost-effective and innovative missions such as satellite launches for communication, navigation, and Earth observation. Landmark achievements like the Mars Orbiter Mission and Chandrayaan lunar missions highlight ISRO’s growing expertise, scientific capability, and contribution to space exploration while supporting education, disaster management, and socio-economic growth in India. disaster management, and socio-economic growth in India.', + readMore: 'RESEARCH AREAS IN 2025', + link: 'https://nitkkr.ac.in/29012020/Research_Areas_in_Space_for_web2023.pdf', + }, + + // For Queries Section + forQueries: { + heading: 'FOR QUERIES', + email: 'racs@nitkkr.ac.in', + }, + }, Hostels: { title: 'Hostels', notificationsTitle: 'Hostel Notifications', diff --git a/i18n/hi.ts b/i18n/hi.ts index 047200d0c..f45a7628c 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -181,6 +181,12 @@ const text: Translations = { timeTable: 'समय-सारणी', }, viewMore: 'और देखें', + buttons: { + hostels: 'छात्रावास', + racs: 'RAC-S (इसरो)', + scoe: 'उत्कृष्टता केंद्र – सीमेंस', + tenders: 'टेंडर', + }, }, Academics: { notifications: 'सूचनाएँ', @@ -687,6 +693,86 @@ const text: Translations = { }, }, }, + + RACS: { + title: 'अंतरिक्ष के लिए क्षेत्रीय शैक्षणिक केंद्र (RAC-S)', + intro: + 'भारतीय अंतरिक्ष कार्यक्रम की भविष्य की तकनीकी एवं कार्यक्रमगत आवश्यकताओं से संबंधित क्षेत्रों में उन्नत अनुसंधान को आगे बढ़ाने की अनिवार्य आवश्यकता को ध्यान में रखते हुए, संस्थान में **रीजनल अकादमिक सेंटर फॉर स्पेस (RAC-S)** की स्थापना की गई है। यह केंद्र **भारतीय अंतरिक्ष अनुसंधान संगठन (ISRO)** और **एनआईटी कुरुक्षेत्र** की एक संयुक्त सहयोगात्मक पहल के रूप में स्थापित किया गया है। इस केंद्र का उद्देश्य देश के उत्तरी क्षेत्र में अंतरिक्ष प्रौद्योगिकी से संबंधित गतिविधियों के संवर्धन के लिए एक उत्प्रेरक की भूमिका निभाना तथा **ISRO** की क्षमता निर्माण, जागरूकता सृजन एवं अनुसंधान एवं विकास (R&D) गतिविधियों के लिए एक प्रतिनिधि (एंबेसडर) के रूप में कार्य करना है।', + notificationsCategory: 'RACS सूचनाएं', + + // Tabs/Navigation + tabs: { + notifications: 'सूचनाएं', + regionalCoordinator: 'क्षेत्रीय समन्वयक', + researchProposalForms: 'अनुसंधान प्रस्ताव फॉर्म', + partnerInstitutes: 'साझेदार संस्थान', + researchAreas: 'अनुसंधान क्षेत्र', + queries: 'प्रश्नों के लिए', + }, + + notifications: { + title: 'सूचनाएं', + }, + // Regional Coordinator Section + coordinator: { + heading: 'क्षेत्रीय समन्वयक', + name: 'प्रो. अरुण गोयल', + position: 'क्षेत्रीय समन्वयक, RAC-S', + email: 'racs@nitkkr.ac.in', + phone: '+91-1744-233XXX', + image: 'fallback/user-image.jpg', + }, + + // Research Proposal Forms Section + researchProposalForms: { + heading: 'अनुसंधान प्रस्ताव प्रपत्र', + + table: { + srno: 'क्र. सं.', + form: 'फॉर्म का नाम', + }, + + formNames: [ + 'निधि अनुदान के लिए आवेदन', + 'ISRO अनुसंधान अनुदान के नियम एवं शर्तें', + 'अन्वेषक(ओं) की जीवनी', + 'अनुसंधान प्रस्ताव (फॉर्म B)', + 'SAC के अनुसंधान क्षेत्र मार्च 2023', + ], + }, + + // Partner Institutes Section + partnerInstitutes: { + heading: 'साझेदार संस्थान', + table: { + srNo: 'क्र. सं.', + institute: 'संस्थान का नाम', + }, + institutes: [ + { name: 'एनआईटी दिल्ली' }, + { name: 'एनआईटी उत्तराखंड' }, + { name: 'डॉ. बी. आर. अंबेडकर राष्ट्रीय प्रौद्योगिकी संस्थान, जालंधर' }, + { name: 'एनआईटी श्रीनगर (जम्मू एवं कश्मीर)' }, + { name: 'कुरुक्षेत्र विश्वविद्यालय, कुरुक्षेत्र' }, + ], + }, + + // Research Areas Section + researchAreas: { + heading: 'अनुसंधान क्षेत्र', + description: + 'भारतीय अंतरिक्ष अनुसंधान संगठन (ISRO) राष्ट्रीय विकास के लिए अंतरिक्ष अनुसंधान एवं प्रौद्योगिकी को आगे बढ़ाने में एक महत्वपूर्ण भूमिका निभाता है। वर्ष 1969 में स्थापित ISRO ने संचार, नेविगेशन तथा पृथ्वी अवलोकन के लिए उपग्रह प्रक्षेपण जैसी किफायती एवं नवाचारी अंतरिक्ष मिशनों के माध्यम से वैश्विक पहचान प्राप्त की है। मंगलयान (मार्स ऑर्बिटर मिशन) और चंद्रयान जैसे ऐतिहासिक मिशन ISRO की बढ़ती विशेषज्ञता, वैज्ञानिक क्षमता और अंतरिक्ष अन्वेषण में उसके योगदान को दर्शाते हैं। इसके साथ ही, ISRO शिक्षा, आपदा प्रबंधन तथा भारत के सामाजिक-आर्थिक विकास में भी महत्वपूर्ण सहयोग प्रदान करता है।', + readMore: 'और पढ़ें', + link: 'https://nitkkr.ac.in/29012020/Research_Areas_in_Space_for_web2023.pdf', + }, + + // For Queries Section + forQueries: { + heading: 'प्रश्नों के लिए', + email: 'racs@nitkkr.ac.in', + }, + }, + Hostels: { title: 'छात्रावास', notificationsTitle: 'छात्रावास सूचनाएँ', diff --git a/i18n/translations.ts b/i18n/translations.ts index e647bbe42..138c67c5a 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -66,6 +66,12 @@ export interface Translations { timeTable: string; }; viewMore: string; + buttons: { + hostels: string; + racs: string; + scoe: string; + tenders: string; + }; }; Academics: { notifications: string; @@ -390,6 +396,77 @@ export interface Translations { }; }; }; + + RACS: { + title: string; + intro: string; + notificationsCategory: string; + + // Tabs/Navigation + tabs: { + notifications: string; + regionalCoordinator: string; + researchProposalForms: string; + partnerInstitutes: string; + researchAreas: string; + queries: string; + }; + // Notifications Section + notifications: { + title: string; + }; + // Regional Coordinator Section + coordinator: { + heading: string; + name: string; + position: string; + email: string; + phone: string; + image: string; + }; + + // Research Proposal Forms Section + researchProposalForms: { + heading: string; + table: { + srno: string; + form: string; + }; + formNames: string[]; + }; + + // Partner Institutes Section + partnerInstitutes: { + heading: string; + table: { + srNo: string; + institute: string; + }; + + institutes: [ + { name: string }, + { name: string }, + { name: string }, + { name: string }, + { name: string }, + ]; + }; + + // Research Areas Section + researchAreas: { + heading: string; + description: string; + readMore: string; + link: string; + }; + + // For Queries Section + forQueries: { + heading: string; + email: string; + }; + }; + Hostels: { title: string; boysHostels: string; diff --git a/package-lock.json b/package-lock.json index a173ae1ab..2107a22c8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10280,4 +10280,4 @@ } } } -} +} \ No newline at end of file From a4e2b521cf71d26a2587e4f85bdc7607bcf413f9 Mon Sep 17 00:00:00 2001 From: Debatreya Das <116421305+Debatreya@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:34:34 +0530 Subject: [PATCH 24/73] Feat/animate (#492): Added home page animations This pull request introduces a new suite of animated UI components using Framer Motion for enhanced user experience and visual appeal across several sections of the application. It replaces static layouts for the header, footer, events grid, page content, and button groups with animated versions, and centralizes animation variants for consistency and reuse. **Major introduction of animated components:** * Added new animated components for header (`animate-header.tsx`), footer (`animate-footer.tsx`), events grid (`animate-events-grid.tsx`), page content (`animate-page-content.tsx`), section wrappers (`animate-section.tsx`), and button groups (`animate-button-group.tsx`). These components use Framer Motion for scroll and hover effects, staggered entrances, and interactive animations. ([app/[locale]/(animations)/animate-header.tsxR1-R48](diffhunk://#diff-1a86ad4d600dec3d9c8f1ad7315a70e2091590b42bc5815e98187555bd38d8eaR1-R48), [app/[locale]/(animations)/animate-footer.tsxR1-R212](diffhunk://#diff-9d03213baa3ffa7524fc544cdaf0f14294dfc0c13c5f6c019d9806127519f0b7R1-R212), [app/[locale]/(animations)/animate-events-grid.tsxR1-R101](diffhunk://#diff-29b5f1de21f3b78582704e76fe491202134537fe1136fc5869f17e936f61b89dR1-R101), [app/[locale]/(animations)/animate-page-content.tsxR1-R118](diffhunk://#diff-0f7d2b5a510dfc56d22049f8bd0241eb380d9af3f324322e89f3ad37c871078cR1-R118), [app/[locale]/(animations)/animate-section.tsxR1-R38](diffhunk://#diff-bbac5636cc02739d7821ca40cf2228bd98ab97ae4c17ea60c682646dc9da0befR1-R38), [app/[locale]/(animations)/animate-button-group.tsxR1-R144](diffhunk://#diff-08236e3f9e6f12ba4d1b37c1e14b47ab72cbe16220ebac30901c5326f9063a32R1-R144)) **Centralized animation configuration:** * Introduced `animation-variants.ts` to define reusable animation variants and viewport settings for scroll-triggered effects, ensuring consistent behavior across all animated components. ([app/[locale]/(animations)/animation-variants.tsR1-R54](diffhunk://#diff-a1592c6dccd58b90189351e0438891d6e258fcb709e8068b75fbdeb40e082a83R1-R54)) **Integration of animated components into pages:** * Updated imports and usage in `events.tsx` and `footer.tsx` to use the new animated components instead of their static counterparts, resulting in animated event grids and animated footer link columns. ([app/[locale]/events.tsxL19-R19](diffhunk://#diff-d8ba86c912049bdd5b256623a1355c1b45682b0855f0fb9d00963a5781250e39L19-R19), [app/[locale]/events.tsxL141-R141](diffhunk://#diff-d8ba86c912049bdd5b256623a1355c1b45682b0855f0fb9d00963a5781250e39L141-R141), [app/[locale]/footer.tsxR15-R21](diffhunk://#diff-61eaebe162b94a33902a9b854548f6d1995339ba17e568a6946ca400d1844292R15-R21), [app/[locale]/footer.tsxL105-R133](diffhunk://#diff-61eaebe162b94a33902a9b854548f6d1995339ba17e568a6946ca400d1844292L105-R133)) **Styling and layout improvements:** * Minor adjustments to layout and spacing in `events.tsx` for improved visual consistency with the new animated components. ([app/[locale]/events.tsxL56-R56](diffhunk://#diff-d8ba86c912049bdd5b256623a1355c1b45682b0855f0fb9d00963a5781250e39L56-R56), [app/[locale]/events.tsxL107-R107](diffhunk://#diff-d8ba86c912049bdd5b256623a1355c1b45682b0855f0fb9d00963a5781250e39L107-R107)) **Index and exports for new animation module:** * Added an index file to export all new animated components and animation variants for easy import throughout the app. ([app/[locale]/(animations)/index.tsR1-R12](diffhunk://#diff-1c924324bea72f4fbaaf39abfa1cfb7a86396006ca5f69ae90c9d301ac320dfbR1-R12)) --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../(animations)/animate-button-group.tsx | 144 ++++++++++++ .../(animations)/animate-events-grid.tsx | 101 +++++++++ app/[locale]/(animations)/animate-footer.tsx | 212 ++++++++++++++++++ app/[locale]/(animations)/animate-header.tsx | 48 ++++ .../(animations)/animate-page-content.tsx | 118 ++++++++++ app/[locale]/(animations)/animate-section.tsx | 38 ++++ .../(animations)/animation-variants.ts | 54 +++++ app/[locale]/(animations)/index.ts | 12 + app/[locale]/events.tsx | 8 +- app/[locale]/footer.tsx | 98 ++++---- app/[locale]/header.tsx | 6 +- app/[locale]/hero-carousel.tsx | 152 +++++++++++++ .../administration/director/page.tsx | 2 +- app/[locale]/page.tsx | 149 ++---------- i18n/en.ts | 2 +- i18n/hi.ts | 2 +- package-lock.json | 32 ++- package.json | 2 +- .../board-of-governors-meetings.schema.ts | 2 +- styles/globals.css | 17 ++ 20 files changed, 987 insertions(+), 212 deletions(-) create mode 100644 app/[locale]/(animations)/animate-button-group.tsx create mode 100644 app/[locale]/(animations)/animate-events-grid.tsx create mode 100644 app/[locale]/(animations)/animate-footer.tsx create mode 100644 app/[locale]/(animations)/animate-header.tsx create mode 100644 app/[locale]/(animations)/animate-page-content.tsx create mode 100644 app/[locale]/(animations)/animate-section.tsx create mode 100644 app/[locale]/(animations)/animation-variants.ts create mode 100644 app/[locale]/(animations)/index.ts create mode 100644 app/[locale]/hero-carousel.tsx diff --git a/app/[locale]/(animations)/animate-button-group.tsx b/app/[locale]/(animations)/animate-button-group.tsx new file mode 100644 index 000000000..abf91cca8 --- /dev/null +++ b/app/[locale]/(animations)/animate-button-group.tsx @@ -0,0 +1,144 @@ +'use client'; + +import { motion, type Variants } from 'framer-motion'; +import Link from 'next/link'; +import { type IconType } from 'react-icons'; + +import { Button } from '~/components/buttons'; +import { cn } from '~/lib/utils'; + +import { viewportSettings } from './animation-variants'; + +// Custom container variants with enhanced stagger +const buttonContainerVariants: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.15, + delayChildren: 0.1, + }, + }, +}; + +// Enhanced button item variants with scale and rotation +const buttonItemVariants: Variants = { + hidden: { + opacity: 0, + y: 60, + scale: 0.8, + rotateX: 15, + }, + visible: { + opacity: 1, + y: 0, + scale: 1, + rotateX: 0, + transition: { + type: 'spring', + stiffness: 100, + damping: 12, + mass: 0.8, + }, + }, +}; + +// Icon animation variants +const iconVariants: Variants = { + hidden: { + scale: 0, + rotate: -180, + }, + visible: { + scale: 1, + rotate: 0, + transition: { + type: 'spring', + stiffness: 200, + damping: 15, + delay: 0.2, + }, + }, +}; + +// Text animation variants +const textVariants: Variants = { + hidden: { + opacity: 0, + y: 10, + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.4, + delay: 0.3, + }, + }, +}; + +interface AnimateButtonGroupProps { + buttonArray: { + label: string; + href: string; + icon: IconType; + }[]; +} + +export default function AnimateButtonGroup({ + buttonArray, +}: AnimateButtonGroupProps) { + return ( + + {buttonArray.map(({ label, href, icon: Icon }, index) => ( + + + + ))} + + ); +} diff --git a/app/[locale]/(animations)/animate-events-grid.tsx b/app/[locale]/(animations)/animate-events-grid.tsx new file mode 100644 index 000000000..56e322af0 --- /dev/null +++ b/app/[locale]/(animations)/animate-events-grid.tsx @@ -0,0 +1,101 @@ +'use client'; + +import { motion } from 'framer-motion'; +import Image from 'next/image'; +import { useState } from 'react'; + +import { Card, CardContent, CardDescription, CardTitle } from '~/components/ui'; +import { cn } from '~/lib/utils'; + +import { EventModal } from '../events/EventModal'; +import type { EventItem } from '../events/EventsList'; +import { + staggerContainerVariants, + staggerItemVariants, + viewportSettings, +} from './animation-variants'; + +interface AnimateEventsGridProps { + events: EventItem[]; + locale: string; + s3Url: string; +} + +export default function AnimateEventsGrid({ + events, + locale, + s3Url, +}: AnimateEventsGridProps) { + const [selectedEvent, setSelectedEvent] = useState(null); + + return ( + <> + + {events.map((event, index) => ( + setSelectedEvent(event)} + variants={staggerItemVariants} + > + + {event.title} 0 + ? event.images[0] + : `${s3Url}/events/${event.startDate.slice(0, 4)}/${event.startDate.slice(5, 7)}/${event.title}/image01.jpg` + } + width={0} + /> + + + + {event.title} + + + {event.description} + + + + + ))} + + + setSelectedEvent(null)} + locale={locale} + /> + + ); +} diff --git a/app/[locale]/(animations)/animate-footer.tsx b/app/[locale]/(animations)/animate-footer.tsx new file mode 100644 index 000000000..584c557ec --- /dev/null +++ b/app/[locale]/(animations)/animate-footer.tsx @@ -0,0 +1,212 @@ +'use client'; + +import { motion, type Variants } from 'framer-motion'; +import Link from 'next/link'; +import { type ReactNode } from 'react'; + +import { viewportSettings } from './animation-variants'; + +// Container for staggered columns - animate from left to right +const footerContainerVariants: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.2, + delayChildren: 0.1, + }, + }, +}; + +// Individual column animation - slide in from left +const columnVariants: Variants = { + hidden: { + opacity: 0, + x: -60, + }, + visible: { + opacity: 1, + x: 0, + transition: { + type: 'spring', + stiffness: 80, + damping: 15, + }, + }, +}; + +// Link item animation +const linkItemVariants: Variants = { + hidden: { + opacity: 0, + x: -20, + }, + visible: { + opacity: 1, + x: 0, + transition: { + duration: 0.3, + }, + }, +}; + +// Links container with stagger +const linksContainerVariants: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.05, + delayChildren: 0.1, + }, + }, +}; + +// Social icons animation +const socialContainerVariants: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.3, + }, + }, +}; + +const socialIconVariants: Variants = { + hidden: { + opacity: 0, + scale: 0, + rotate: -180, + }, + visible: { + opacity: 1, + scale: 1, + rotate: 0, + transition: { + type: 'spring', + stiffness: 200, + damping: 15, + }, + }, +}; + +interface FooterLinkColumnProps { + title: string; + links: { name: string; href: string }[]; + locale: string; + className?: string; +} + +export function AnimateFooterLinkColumn({ + title, + links, + locale, + className, +}: FooterLinkColumnProps) { + return ( + + + {title} + + + {links.map((item, index) => ( + + + {item.name} + + + ))} + + + ); +} + +interface AnimateFooterLinksGridProps { + children: ReactNode; + className?: string; +} + +export function AnimateFooterLinksGrid({ + children, + className, +}: AnimateFooterLinksGridProps) { + return ( + + {children} + + ); +} + +interface AnimateSocialIconsProps { + children: ReactNode; + className?: string; +} + +export function AnimateSocialIcons({ + children, + className, +}: AnimateSocialIconsProps) { + return ( + + {children} + + ); +} + +export function AnimateSocialIcon({ children }: { children: ReactNode }) { + return ( + + {children} + + ); +} diff --git a/app/[locale]/(animations)/animate-header.tsx b/app/[locale]/(animations)/animate-header.tsx new file mode 100644 index 000000000..99df7f6cf --- /dev/null +++ b/app/[locale]/(animations)/animate-header.tsx @@ -0,0 +1,48 @@ +'use client'; + +import { motion, type Variants } from 'framer-motion'; +import { type ReactNode } from 'react'; + +import { cn } from '~/lib/utils'; + +// Header slide down animation +const headerVariants: Variants = { + hidden: { + y: -100, + opacity: 0, + }, + visible: { + y: 0, + opacity: 1, + transition: { + type: 'spring', + stiffness: 100, + damping: 20, + mass: 1, + }, + }, +}; + +interface AnimateHeaderProps { + children: ReactNode; + className?: string; +} + +export default function AnimateHeader({ + children, + className, +}: AnimateHeaderProps) { + return ( + + {children} + + ); +} diff --git a/app/[locale]/(animations)/animate-page-content.tsx b/app/[locale]/(animations)/animate-page-content.tsx new file mode 100644 index 000000000..572d51d5d --- /dev/null +++ b/app/[locale]/(animations)/animate-page-content.tsx @@ -0,0 +1,118 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { + TbBuildingSkyscraper, + TbContract, + TbRocket, + TbSchool, +} from 'react-icons/tb'; +import { type ReactNode } from 'react'; + +import Heading from '~/components/heading'; +import MessageCard from '~/components/message-card'; + +import { AnimateButtonGroup, AnimateSection } from '.'; +import { fadeUpVariants, viewportSettings } from './animation-variants'; + +interface AnimatePageContentProps { + locale: string; + text: { + director: { + title: string; + name: string; + quote: string[]; + more: string; + }; + buttons: { + hostels: string; + racs: string; + scoe: string; + tenders: string; + }; + }; + notificationsSection: ReactNode; + eventsSection: ReactNode; +} + +export default function AnimatePageContent({ + locale, + text, + notificationsSection, + eventsSection, +}: AnimatePageContentProps) { + return ( + <> + {/* Notifications Section - Animated Wrapper */} + + {notificationsSection} + + + {/* Events Section - Animated Wrapper */} + + {eventsSection} + + + {/* Director's Corner Section */} + + + + + + {/* Button Group with Staggered Animation */} + + + ); +} diff --git a/app/[locale]/(animations)/animate-section.tsx b/app/[locale]/(animations)/animate-section.tsx new file mode 100644 index 000000000..26efa270f --- /dev/null +++ b/app/[locale]/(animations)/animate-section.tsx @@ -0,0 +1,38 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { type ReactNode } from 'react'; + +import { fadeUpVariants, viewportSettings } from './animation-variants'; + +interface AnimateSectionProps { + children: ReactNode; + className?: string; + id?: string; + style?: React.CSSProperties; + as?: 'section' | 'article' | 'div'; +} + +export default function AnimateSection({ + children, + className, + id, + style, + as = 'section', +}: AnimateSectionProps) { + const Component = motion[as]; + + return ( + + {children} + + ); +} diff --git a/app/[locale]/(animations)/animation-variants.ts b/app/[locale]/(animations)/animation-variants.ts new file mode 100644 index 000000000..b80720220 --- /dev/null +++ b/app/[locale]/(animations)/animation-variants.ts @@ -0,0 +1,54 @@ +// Animation variants for scroll reveal effects +import { type Variants } from 'framer-motion'; + +// Fade up animation for sections +export const fadeUpVariants: Variants = { + hidden: { + opacity: 0, + y: 40, + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.6, + ease: 'easeOut', + }, + }, +}; + +// Container variant for staggered children +export const staggerContainerVariants: Variants = { + hidden: { + opacity: 0, + }, + visible: { + opacity: 1, + transition: { + staggerChildren: 0.1, + delayChildren: 0.1, + }, + }, +}; + +// Child variant for staggered items +export const staggerItemVariants: Variants = { + hidden: { + opacity: 0, + y: 40, + }, + visible: { + opacity: 1, + y: 0, + transition: { + duration: 0.5, + ease: 'easeOut', + }, + }, +}; + +// Viewport settings for whileInView +export const viewportSettings = { + once: true, + amount: 0.2, +} as const; diff --git a/app/[locale]/(animations)/index.ts b/app/[locale]/(animations)/index.ts new file mode 100644 index 000000000..48885dacb --- /dev/null +++ b/app/[locale]/(animations)/index.ts @@ -0,0 +1,12 @@ +export { default as AnimateSection } from './animate-section'; +export { default as AnimateButtonGroup } from './animate-button-group'; +export { default as AnimateEventsGrid } from './animate-events-grid'; +export { default as AnimatePageContent } from './animate-page-content'; +export { default as AnimateHeader } from './animate-header'; +export { + AnimateFooterLinkColumn, + AnimateFooterLinksGrid, + AnimateSocialIcons, + AnimateSocialIcon, +} from './animate-footer'; +export * from './animation-variants'; diff --git a/app/[locale]/events.tsx b/app/[locale]/events.tsx index 9ee54e2af..45a8fe355 100644 --- a/app/[locale]/events.tsx +++ b/app/[locale]/events.tsx @@ -16,7 +16,7 @@ import { cn } from '~/lib/utils'; import { db, type eventCategoryEnum } from '~/server/db'; import { getS3Url } from '~/server/s3'; -import { EventsGrid } from './EventsGrid'; +import { AnimateEventsGrid } from './(animations)'; type Cat = (typeof eventCategoryEnum.enumValues)[number]; @@ -53,7 +53,7 @@ export default async function Events({ return (
    ))} -
    +
    - // - // - // - // - // {departments.map(({ name, urlName }, index) => ( - //
    - // - // - // {name} - // - //
    - // ))} - //
    - // - // ) : ( - //
      - // {departments.map(({ name, urlName }, index) => ( - //
    1. - // - //
      - //
      - // - //
      - // {name} - //
      - //
      - //
    2. - // ))} - //
    - // ); return ( ) : ( - filteredFaculty.map((faculty, index) => { - const isDepartmentHead = departmentHeads.find( - ({ facultyId }) => facultyId === faculty.id - ); - - const profileExternalLinks = { - googleScholarId: faculty.googleScholarId, - linkedInId: faculty.linkedInId, - researchGateId: faculty.researchGateId, - scopusId: faculty.scopusId, - }; - - return ( -
  • - { + const aIsHead = departmentHeads.some( + ({ facultyId }) => facultyId === a.id + ); + const bIsHead = departmentHeads.some( + ({ facultyId }) => facultyId === b.id + ); + return aIsHead === bIsHead ? 0 : aIsHead ? -1 : 1; + }) + .map((faculty, index) => { + const isDepartmentHead = departmentHeads.find( + ({ facultyId }) => facultyId === faculty.id + ); + + const profileExternalLinks = { + googleScholarId: faculty.googleScholarId, + linkedInId: faculty.linkedInId, + researchGateId: faculty.researchGateId, + scopusId: faculty.scopusId, + }; + + return ( +
  • - -
    -
    -

    {faculty.person.name}

    -

    - {faculty.designation} - {isDepartmentHead && ` (${deptartmentHeadText})`} -

    -
    - {/* Contact Information */} -
      -
    • - - {faculty.person.email} -
    • -
    • - - {faculty.person.telephone} -
    • -
    - {/* Areas of Interest */} - {faculty.areasOfInterest && - faculty.areasOfInterest.length > 0 && ( -
    -
      - {faculty.areasOfInterest - .slice(0, 1) - .map((area, index) => ( -
    • {area}
    • - ))} - {faculty.areasOfInterest.length > 1 && ( -
    • - {faculty.areasOfInterest[1]}{' '} - {faculty.areasOfInterest.length > 2 && ( - - + {faculty.areasOfInterest.length - 2} more - - )} -
    • - )} -
    -
    - )} -
    - {/* Links */} - {/* On large screen */} -
    + + +
    +
    +

    {faculty.person.name}

    +

    + {faculty.designation} + {isDepartmentHead && ` (${deptartmentHeadText})`} +

    +
    + {/* Contact Information */} +
      +
    • + + {faculty.person.email} +
    • +
    • + + {faculty.person.telephone} +
    • +
    + {/* Areas of Interest */} + {faculty.areasOfInterest && + faculty.areasOfInterest.length > 0 && ( +
    +
      + {faculty.areasOfInterest + .slice(0, 1) + .map((area, index) => ( +
    • {area}
    • + ))} + {faculty.areasOfInterest.length > 1 && ( +
    • + {faculty.areasOfInterest[1]}{' '} + {faculty.areasOfInterest.length > 2 && ( + + + {faculty.areasOfInterest.length - 2} more + + )} +
    • + )} +
    +
    + )} +
    + {/* Links */} + {/* On large screen */} +
    + {( + Object.entries(profileExternalLinks) as [ + keyof typeof profileExternalLinks, + string, + ][] + ).map(([key, value]) => { + if (key in profileExternalLinks) { + return ( + + {key} + {text.externalLinks[key]} + + ); + } + })} +
    + {/* On small and medium screens */} + +
    {( Object.entries(profileExternalLinks) as [ keyof typeof profileExternalLinks, @@ -474,21 +443,15 @@ const FacultyList = async ({ return ( {key} {text.externalLinks[key]} @@ -496,38 +459,9 @@ const FacultyList = async ({ } })}
    - {/* On small and medium screens */} - -
    - {( - Object.entries(profileExternalLinks) as [ - keyof typeof profileExternalLinks, - string, - ][] - ).map(([key, value]) => { - if (key in profileExternalLinks) { - return ( - - {key} - {text.externalLinks[key]} - - ); - } - })} -
    -
  • - ); - }) + + ); + }) ); }; diff --git a/app/[locale]/faculty-and-staff/utils.tsx b/app/[locale]/faculty-and-staff/utils.tsx index cde68d844..a6af679c6 100644 --- a/app/[locale]/faculty-and-staff/utils.tsx +++ b/app/[locale]/faculty-and-staff/utils.tsx @@ -147,6 +147,12 @@ export async function FacultyOrStaffComponent({ sql`continuing_education.faculty_id = faculty.employee_id` ) .as('continuingEducation'), + doctoralStudents: db + .$count( + researchScholars, + sql`research_scholars.faculty_id = faculty.employee_id` + ) + .as('doctoralStudents'), }, }); @@ -192,7 +198,6 @@ export async function FacultyOrStaffComponent({ ); const facultyDescription = { - doctoralStudents: 0, // Doctoral Student count not implemented ...facultyDescriptionTmp, // Original faculty details publications: realPublicationsCount, // Use the new count method }; diff --git a/server/actions/notifications.ts b/server/actions/notifications.ts index d6e1910d1..7fb175c49 100644 --- a/server/actions/notifications.ts +++ b/server/actions/notifications.ts @@ -137,7 +137,9 @@ export async function loadMoreNotifications( // Add category filter at DB level if (categories?.length) { - conditions.push(arrayOverlaps(notifications.categories, categories as Cat[])); + conditions.push( + arrayOverlaps(notifications.categories, categories as Cat[]) + ); } // Fetch batch + 1 to check if there are more From ac02cc5b5507589edb84ca0a2bef4e2dc5bc625a Mon Sep 17 00:00:00 2001 From: Debatreya Das <116421305+Debatreya@users.noreply.github.com> Date: Wed, 28 Jan 2026 04:26:44 +0530 Subject: [PATCH 33/73] fix: tnp placement stants redirection fixed (#508) This pull request updates the placement statistics section on the Training and Placement page to streamline how PDF links are generated and displayed, and also updates the English and Hindi translations for placement stats labels to be more consistent and up-to-date. **Placement stats PDF link handling:** - Centralizes the PDF base URL generation using the `getS3Url()` helper, and creates a `placementStats` array to hold all PDF URLs for placement statistics. This replaces the previous hardcoded URLs throughout the component. ([app/[locale]/training-and-placement/page.tsxR20-R40](diffhunk://#diff-3d46475657a686b8cc7d39b08ef532119e5a09c372f55bc1d9aec8e24f61d72aR20-R40)) - Updates the `ButtonGroup` component to dynamically generate its buttons by mapping over the `placementStats` array and corresponding translation labels, instead of using a manually constructed array. This makes the code more maintainable and ensures the links and labels stay in sync. ([app/[locale]/training-and-placement/page.tsxL83-R107](diffhunk://#diff-3d46475657a686b8cc7d39b08ef532119e5a09c372f55bc1d9aec8e24f61d72aL83-R107)) **Translation updates:** - Adds new entries for the 2024-25 and 2023-24 academic sessions and standardizes the naming format for all placement stats labels in both English (`i18n/en.ts`) and Hindi (`i18n/hi.ts`). This ensures the labels match the new files and are consistent in formatting. [[1]](diffhunk://#diff-8ee4c65eaaf735c33bd37bf76d2a87fb76d02b7928096cc26db6364694988ceeR1958-R1968) [[2]](diffhunk://#diff-912c4f60653cc2c8d00fda0b21a7a65ab1c3e1c3f65007e1f211f40c951596b8R1937-R1947) --- app/[locale]/training-and-placement/page.tsx | 70 +++++++------------- i18n/en.ts | 14 ++-- i18n/hi.ts | 14 ++-- 3 files changed, 40 insertions(+), 58 deletions(-) diff --git a/app/[locale]/training-and-placement/page.tsx b/app/[locale]/training-and-placement/page.tsx index 526e91825..3be71e618 100644 --- a/app/[locale]/training-and-placement/page.tsx +++ b/app/[locale]/training-and-placement/page.tsx @@ -17,9 +17,27 @@ import { AccordionTrigger, } from '~/components/ui/accordion'; import { getTranslations } from '~/i18n/translations'; +import { getS3Url } from '~/server/s3'; import clients from './recruiters'; +// Hardcoded PDF base URL and PDF list for placement stats +const pdfBase = `${getS3Url()}/training-and-placement/placement-stats/`; + +const placementStats: string[] = [ + `${pdfBase}Academic-Session-2024-25.pdf`, + `${pdfBase}Academic-Session-2023-24.pdf`, + `${pdfBase}Academic-Session-2022-23.pdf`, + `${pdfBase}Academic-Session-2021-22.pdf`, + `${pdfBase}Academic-Session-2020-21-FN.pdf`, + `${pdfBase}Academic-Session-2019-20-FN.pdf`, + `${pdfBase}Academic-Session-2018-19-FN.pdf`, + `${pdfBase}Academic-Session-2017-18.pdf`, + `${pdfBase}Academic-Session-2017-18.pdf`, + `${pdfBase}Academic-Session-2017-18-FN.pdf`, + `${pdfBase}Academic-Session-2016-17.pdf`, +]; + export default async function TrainingAndPlacement({ params: { locale }, }: { @@ -80,53 +98,13 @@ export default async function TrainingAndPlacement({ }> ({ + label, + href: placementStats[index], icon: MdArticle, - }, - ]} + }))} />
    diff --git a/i18n/en.ts b/i18n/en.ts index ad9bbadbe..b34a5a122 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -1955,15 +1955,17 @@ Centre/empanelled hospital/Govt. hospital after giving preliminary treatment.`, }, stats: { content: [ - `Academic Session 2022-23 `, + `Academic Session 2024-25`, + `Academic Session 2023-24`, + `Academic Session 2022-23`, `Academic Session 2021-22`, `Academic Session 2020-21 FN`, - `Academic-Session-2019-20 FN `, + `Academic-Session-2019-20 FN`, `Academic Session 2018-19 FN`, - `Academic Session 2018_19`, - `Academic Session 2017_18`, - `Academic Session 2017-18 FN `, - `Academic Session 2016_17`, + `Academic Session 2018-19`, + `Academic Session 2017-18`, + `Academic Session 2017-18 FN`, + `Academic Session 2016-17`, ], }, ourrecruiters: { diff --git a/i18n/hi.ts b/i18n/hi.ts index aa502edbe..ca6d0c6dd 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -1934,15 +1934,17 @@ const text: Translations = { }, stats: { content: [ - `शैक्षिक सत्र 2022-23 `, + `शैक्षिक सत्र 2024-25`, + `शैक्षिक सत्र 2023-24`, + `शैक्षिक सत्र 2022-23`, `शैक्षिक सत्र 2021-22`, `शैक्षिक सत्र 2020-21 FN`, - `शैक्षिक सत्र 2019-20 FN `, + `शैक्षिक सत्र 2019-20 FN`, `शैक्षिक सत्र 2018-19 FN`, - `शैक्षिक सत्र 2018_19`, - `शैक्षिक सत्र 2017_18`, - `शैक्षिक सत्र 2017-18 FN `, - `शैक्षिक सत्र 2016_17`, + `शैक्षिक सत्र 2018-19`, + `शैक्षिक सत्र 2017-18`, + `शैक्षिक सत्र 2017-18 FN`, + `शैक्षिक सत्र 2016-17`, ], }, ourrecruiters: { From 94b9a2fad9a4320e3d898cf8ee7297a8124b8da6 Mon Sep 17 00:00:00 2001 From: Debatreya Date: Wed, 28 Jan 2026 06:18:10 +0530 Subject: [PATCH 34/73] feat: Thought Lab Added --- .../(animations)/animate-page-content.tsx | 20 +-- app/[locale]/student-activities/page.tsx | 9 +- .../student-activities/thought-lab/page.tsx | 170 ++++++++++++++++++ i18n/en.ts | 80 ++++++++- i18n/hi.ts | 73 +++++++- i18n/translations.ts | 31 +++- server/s3/index.ts | 1 + server/s3/list-folder-images.ts | 85 +++++++++ 8 files changed, 447 insertions(+), 22 deletions(-) create mode 100644 app/[locale]/student-activities/thought-lab/page.tsx create mode 100644 server/s3/list-folder-images.ts diff --git a/app/[locale]/(animations)/animate-page-content.tsx b/app/[locale]/(animations)/animate-page-content.tsx index 572d51d5d..d2f7da097 100644 --- a/app/[locale]/(animations)/animate-page-content.tsx +++ b/app/[locale]/(animations)/animate-page-content.tsx @@ -2,8 +2,8 @@ import { motion } from 'framer-motion'; import { + TbBrain, TbBuildingSkyscraper, - TbContract, TbRocket, TbSchool, } from 'react-icons/tb'; @@ -28,7 +28,7 @@ interface AnimatePageContentProps { hostels: string; racs: string; scoe: string; - tenders: string; + thoughtLab: string; }; }; notificationsSection: ReactNode; @@ -91,11 +91,6 @@ export default function AnimatePageContent({ {/* Button Group with Staggered Animation */} diff --git a/app/[locale]/student-activities/page.tsx b/app/[locale]/student-activities/page.tsx index 538321dda..e1acf9d2c 100644 --- a/app/[locale]/student-activities/page.tsx +++ b/app/[locale]/student-activities/page.tsx @@ -27,10 +27,13 @@ export default async function StudentActivities({ + + +
    + {/* Main Title with dual elephants */} + + + {/* About Description */} +

    + A Thought Laboratory (Thought Lab) has been set up in our institute, + which was inaugurated by the Hon'ble Governor of Haryana on May + 10, 2022. The idea of Thought Lab is to train people on how to + cultivate positive and creative thoughts and contribute positively at + their own homes, organizations, and within society as a whole. +

    + + {/* Vision & Mission Section */} +
    +
      +
    • +

      Vision

      + {text.ThoughtLab.vision.map((item, index) => ( +

      + {item} +

      + ))} +
    • +
    • +

      Mission

      + {text.ThoughtLab.mission.map((item, index) => ( +

      + {item} +

      + ))} +
    • +
    + + {/* Thought Lab */} +
    +
    + + {/* Faculty & Student Secretaries */} +
    + +

    + Faculty & Student Secretaries of Thought Lab for Session 2025-26 +

    + +
    + + + + + + + + + + {text.ThoughtLab.secretaries.faculty_secretaries.map( + (member, index) => ( + + + + + + ) + )} + {text.ThoughtLab.secretaries.student_secretaries.map( + (member, index) => ( + + + + + + ) + )} + +
    S.No.NameDesignation
    {index + 1}{member.name}{member.designation}
    + {text.ThoughtLab.secretaries.faculty_secretaries.length + + index + + 1} + {member.name}{member.designation}
    +
    +
    + + {/* Purpose */} +
    + +
      + {text.ThoughtLab.purpose.map((item, index) => ( +
    • {item}
    • + ))} +
    +
    + + {/* Benefits */} +
    + +
      + {text.ThoughtLab.benefits.map((item, index) => ( +
    • {item}
    • + ))} +
    +
    + + {/* Contact Us */} +
    + +

    {text.ThoughtLab.contact.office}

    +

    + Website:{' '} + + {text.ThoughtLab.contact.website} + +

    +
    + + {/* Gallery */} +
    + + +
    + + ); +} diff --git a/i18n/en.ts b/i18n/en.ts index b34a5a122..6c49845a8 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -187,10 +187,10 @@ const text: Translations = { }, viewMore: 'View More', buttons: { - hostels: 'Hostels', racs: 'RAC-S (ISRO)', scoe: 'CoE (Siemens)', - tenders: 'Tenders', + thoughtLab: 'Thought Lab', + hostels: 'Hostels', }, }, Academics: { @@ -237,6 +237,67 @@ const text: Translations = { whyToJoinUs: 'Why Join Us?', }, Clubs: { title: 'CLUBS' }, + ThoughtLab: { + title: 'Thought Lab', + vision: [ + 'To empower the thoughts of youth to build a world of peace, love and universal harmony through the means of science and spirituality.', + ], + mission: [ + 'To create a spiritual ambience for learning meditation and experiencing inner peace.', + 'To provide a platform for research on Spiritual Dimensions of life.', + 'To gain insights on Holistic development through Meditation.', + 'To enable individuals to gain control over their thoughts, feelings and emotions.', + 'To create interest among youth for Values & Spirituality.', + ], + secretaries: { + faculty_secretaries: [ + { name: 'Dr. Dixit Garg', designation: 'Coordinator, Thought Lab' }, + { + name: 'Dr. Than Singh Saini', + designation: 'Co-Coordinator, Thought Lab', + }, + { + name: 'Dr. Anshu Parashar', + designation: 'Co-Coordinator, Thought Lab', + }, + { name: 'Dr. Jagan Nath', designation: 'Co-Coordinator, Thought Lab' }, + { name: 'Ms. Anjali Taneja', designation: 'Counsellor Psychologist' }, + { name: 'Renu Munjal', designation: 'Technician, Thought Lab' }, + ], + student_secretaries: [ + { + name: 'Ashish Saini', + designation: 'Students Secretary, Thought Lab', + }, + { name: 'Bhawna', designation: 'Students Secretary, Thought Lab' }, + { name: 'Rajnish', designation: 'Students Secretary, Thought Lab' }, + { + name: 'Vanshika Arora', + designation: 'Students Secretary, Thought Lab', + }, + ], + }, + purpose: [ + 'To provide a place for learning meditation and spiritual concepts.', + 'To create spiritual ambience for experiencing inner peace.', + 'To provide a platform for research on Spiritual Dimensions of life.', + 'To gain insights on Holistic development through Meditation.', + 'To build value based environment among members of organization.', + 'To create interest among youth for Values & Spirituality.', + ], + benefits: [ + 'Experience Peace and Empower the Self', + 'Positive change in personality', + 'Freedom from stress, anxiety and fear', + 'Enhance focus and concentration', + 'Improve decision-making power', + 'Opportunity to work on Spiritual projects', + ], + contact: { + office: 'Office of Thought Lab, NIT Kurukshetra', + website: 'https://thought-labv2.netlify.app/', + }, + }, Committee: { building: 'BUILDING & WORK COMMITTEE', financial: 'FINANCIAL COMMITTEE', @@ -1768,7 +1829,20 @@ Centre/empanelled hospital/Govt. hospital after giving preliminary treatment.`, nss: 'NSS', ncc: 'NCC', }, - sections: { clubs: { title: 'CLUBS', more: 'Explore all clubs' } }, + sections: { + clubs: { title: 'CLUBS', more: 'Explore all clubs' }, + council: { + title: 'STUDENT COUNCIL', + more: 'Explore all student council activities', + }, + events: { title: 'EVENTS', more: 'Explore all events' }, + thoughtLab: { + title: 'THOUGHT LAB', + more: 'Explore all thought lab activities', + }, + nss: { title: 'NSS', more: 'Explore all NSS activities' }, + ncc: { title: 'NCC', more: 'Explore all NCC activities' }, + }, }, DirectorMessage: { title: `Director's Message`, diff --git a/i18n/hi.ts b/i18n/hi.ts index ca6d0c6dd..7436f0216 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -182,10 +182,10 @@ const text: Translations = { }, viewMore: 'और देखें', buttons: { - hostels: 'छात्रावास', racs: 'RAC-S (इसरो)', - scoe: 'उत्कृष्टता केंद्र – सीमेंस', - tenders: 'टेंडर', + scoe: 'उत्कृष्टता केंद्र - सीमेंस', + thoughtLab: 'थॉट लैब', + hostels: 'छात्रावास', }, }, Academics: { @@ -234,6 +234,58 @@ const text: Translations = { whyToJoinUs: 'हमारे साथ क्यों जुड़ें', }, Clubs: { title: 'संघठनें' }, + ThoughtLab: { + title: 'विचार प्रयोगशाला', + vision: [ + 'विज्ञान और आध्यात्मिकता के माध्यम से शांति, प्रेम और सार्वभौमिक सामंजस्य की दुनिया बनाने के लिए युवाओं के विचारों को सशक्त बनाना।', + ], + mission: [ + 'ध्यान सीखने और आंतरिक शांति का अनुभव करने के लिए आध्यात्मिक वातावरण बनाना।', + 'जीवन के आध्यात्मिक आयामों पर अनुसंधान के लिए एक मंच प्रदान करना।', + 'ध्यान के माध्यम से समग्र विकास पर अंतर्दृष्टि प्राप्त करना।', + 'व्यक्तियों को अपने विचारों, भावनाओं और संवेगों पर नियंत्रण पाने में सक्षम बनाना।', + 'युवाओं में मूल्यों और आध्यात्मिकता के प्रति रुचि पैदा करना।', + ], + secretaries: { + faculty_secretaries: [ + { name: 'डॉ. दीक्षित गर्ग', designation: 'समन्वयक, विचार प्रयोगशाला' }, + { + name: 'डॉ. थान सिंह सैनी', + designation: 'सह-समन्वयक, विचार प्रयोगशाला', + }, + { name: 'डॉ. अंशु पराशर', designation: 'सह-समन्वयक, विचार प्रयोगशाला' }, + { name: 'डॉ. जगन नाथ', designation: 'सह-समन्वयक, विचार प्रयोगशाला' }, + { name: 'सुश्री अंजलि तनेजा', designation: 'परामर्शदाता मनोवैज्ञानिक' }, + { name: 'रेनू मुंजाल', designation: 'तकनीशियन, विचार प्रयोगशाला' }, + ], + student_secretaries: [ + { name: 'आशीष सैनी', designation: 'छात्र सचिव, विचार प्रयोगशाला' }, + { name: 'भावना', designation: 'छात्र सचिव, विचार प्रयोगशाला' }, + { name: 'रजनीश', designation: 'छात्र सचिव, विचार प्रयोगशाला' }, + { name: 'वंशिका अरोड़ा', designation: 'छात्र सचिव, विचार प्रयोगशाला' }, + ], + }, + purpose: [ + 'ध्यान और आध्यात्मिक अवधारणाओं को सीखने के लिए एक स्थान प्रदान करना।', + 'आंतरिक शांति का अनुभव करने के लिए आध्यात्मिक वातावरण बनाना।', + 'जीवन के आध्यात्मिक आयामों पर अनुसंधान के लिए एक मंच प्रदान करना।', + 'ध्यान के माध्यम से समग्र विकास पर अंतर्दृष्टि प्राप्त करना।', + 'संगठन के सदस्यों के बीच मूल्य आधारित वातावरण बनाना।', + 'युवाओं में मूल्यों और आध्यात्मिकता के प्रति रुचि पैदा करना।', + ], + benefits: [ + 'शांति का अनुभव करें और स्वयं को सशक्त बनाएं', + 'व्यक्तित्व में सकारात्मक बदलाव', + 'तनाव, चिंता और भय से मुक्ति', + 'एकाग्रता और ध्यान केंद्रित करने की क्षमता बढ़ाएं', + 'निर्णय लेने की शक्ति में सुधार', + 'आध्यात्मिक परियोजनाओं पर काम करने का अवसर', + ], + contact: { + office: 'विचार प्रयोगशाला कार्यालय, एनआईटी कुरुक्षेत्र', + website: 'https:/thought-labv2.netlify.app/', + }, + }, Committee: { building: 'निर्माण एवं कार्य समिति', financial: 'वित्तीय समिति', @@ -1743,7 +1795,20 @@ const text: Translations = { nss: 'एनएसएस', ncc: 'एनसीसी', }, - sections: { clubs: { title: 'संघठनें', more: 'सभी संघठनो को तलाशें' } }, + sections: { + clubs: { title: 'संघठनें', more: 'सभी संघठनो को तलाशें' }, + council: { + title: 'छात्र परिषद', + more: 'सभी छात्र परिषद गतिविधियों को तलाशें', + }, + events: { title: 'आयोजनाएँ', more: 'सभी आयोजनों को तलाशें' }, + thoughtLab: { + title: 'विचार प्रयोगशाला', + more: 'सभी विचार प्रयोगशाला गतिविधियों को तलाशें', + }, + nss: { title: 'एनएसएस', more: 'सभी एनएसएस गतिविधियों को तलाशें' }, + ncc: { title: 'एनसीसी', more: 'सभी एनसीसी गतिविधियों को तलाशें' }, + }, }, DirectorMessage: { title: 'निदेशक महोदय का संदेश', diff --git a/i18n/translations.ts b/i18n/translations.ts index 1200a2526..cdbfac156 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -67,10 +67,10 @@ export interface Translations { }; viewMore: string; buttons: { - hostels: string; racs: string; scoe: string; - tenders: string; + thoughtLab: string; + hostels: string; }; }; Academics: { @@ -113,6 +113,28 @@ export interface Translations { whyToJoinUs: string; }; Clubs: { title: string }; + ThoughtLab: { + title: string; + vision: string[]; + mission: string[]; + secretaries: { + faculty_secretaries: { + name: string; + designation: string; + }[]; + student_secretaries: { + name: string; + designation: string; + }[]; + }; + purpose: string[]; + benefits: string[]; + contact: { + office: string; + website: string; + }; + }; + Committee: { building: string; financial: string; @@ -1101,6 +1123,11 @@ export interface Translations { }; sections: { clubs: { title: string; more: string }; + council: { title: string; more: string }; + events: { title: string; more: string }; + thoughtLab: { title: string; more: string }; + nss: { title: string; more: string }; + ncc: { title: string; more: string }; }; }; Research: { diff --git a/server/s3/index.ts b/server/s3/index.ts index 853dadb72..1785ea4fd 100644 --- a/server/s3/index.ts +++ b/server/s3/index.ts @@ -25,4 +25,5 @@ export const getS3Url = (type: 'private' | 'public' = 'public') => : `https://${env.AWS_PRIVATE_S3_NAME}.s3.${env.AWS_S3_REGION}.amazonaws.com/isaac-s3-images`; export * from './count-children'; +export * from './list-folder-images'; export * from './upload'; diff --git a/server/s3/list-folder-images.ts b/server/s3/list-folder-images.ts new file mode 100644 index 000000000..694c11742 --- /dev/null +++ b/server/s3/list-folder-images.ts @@ -0,0 +1,85 @@ +import { ListObjectsV2Command } from '@aws-sdk/client-s3'; + +import { env } from '~/lib/env/server'; + +import { s3 } from '.'; + +const DEFAULT_IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.webp']; + +/** + * Lists all image files in a given S3 folder (non-recursive) that match the allowed extensions. + * + * @param folder - The folder path in S3 (e.g., 'institute/cells/iks/') + * @param allowedExtensions - Array of allowed file extensions (default: ['.png', '.jpg', '.jpeg', '.webp']) + * @param bucket - The S3 bucket type ('public' or 'private', default: 'public') + * @returns An array of objects with `src` property containing the relative path of each image + */ +export const listFolderImages = async ( + folder: string, + allowedExtensions: string[] = DEFAULT_IMAGE_EXTENSIONS, + bucket: 'private' | 'public' = 'public' +): Promise<{ src: string }[]> => { + const images: { src: string }[] = []; + let continuationToken: string | undefined; + + // Ensure folder path ends with '/' + const normalizedFolder = folder.endsWith('/') ? folder : `${folder}/`; + + // Normalize extensions to lowercase + const normalizedExtensions = allowedExtensions.map((ext) => + ext.toLowerCase().startsWith('.') + ? ext.toLowerCase() + : `.${ext.toLowerCase()}` + ); + + do { + const response = await s3.send( + new ListObjectsV2Command({ + Bucket: + bucket === 'public' + ? env.AWS_PUBLIC_S3_NAME + : env.AWS_PRIVATE_S3_NAME, + Prefix: `isaac-s3-images/${normalizedFolder}`, + ContinuationToken: continuationToken, + Delimiter: '/', // This ensures we only get immediate children (not recursive) + }) + ); + + if (response.Contents) { + for (const object of response.Contents) { + if (!object.Key) continue; + + // Get the filename from the full key + const key = object.Key; + const fileName = key.split('/').pop(); + + if (!fileName) continue; + + // Check if the file has an allowed extension + const hasAllowedExtension = normalizedExtensions.some((ext) => + fileName.toLowerCase().endsWith(ext) + ); + + if (hasAllowedExtension) { + // Remove the 'isaac-s3-images/' prefix to get the relative path + const relativePath = key.replace('isaac-s3-images/', ''); + images.push({ src: relativePath }); + } + } + } + + continuationToken = response.NextContinuationToken; + } while (continuationToken); + + // Sort images naturally (by filename) + images.sort((a, b) => { + const nameA = a.src.split('/').pop() ?? ''; + const nameB = b.src.split('/').pop() ?? ''; + return nameA.localeCompare(nameB, undefined, { + numeric: true, + sensitivity: 'base', + }); + }); + + return images; +}; From 53d352d10ca43845ff2c0489a294be3e0ba3c43d Mon Sep 17 00:00:00 2001 From: Debatreya Date: Wed, 28 Jan 2026 06:49:24 +0530 Subject: [PATCH 35/73] chore: Director message updated --- .../administration/director/director-card.tsx | 9 ++--- i18n/en.ts | 33 +++++++++++++------ i18n/hi.ts | 28 ++++++++++++---- 3 files changed, 48 insertions(+), 22 deletions(-) diff --git a/app/[locale]/institute/administration/director/director-card.tsx b/app/[locale]/institute/administration/director/director-card.tsx index 765b8bbb1..66009f53b 100644 --- a/app/[locale]/institute/administration/director/director-card.tsx +++ b/app/[locale]/institute/administration/director/director-card.tsx @@ -58,7 +58,7 @@ export default function DirectorCard({
    • {labels.phoneNo}{' '} - {phone} + {phone}
    • {labels.faxNo}{' '} @@ -66,14 +66,11 @@ export default function DirectorCard({
    • {labels.mobileNo}{' '} - {mobile} + {mobile}
    • {labels.emailId}{' '} - + {email}
    • diff --git a/i18n/en.ts b/i18n/en.ts index 6c49845a8..d7a9b7db1 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -142,14 +142,7 @@ const text: Translations = { title: 'DIRECTOR’S CORNER', name: 'Prof. B. V. Ramana Reddy', quote: [ - `India, the land of seekers, is at the cusp of becoming Vishwa Guru all - over again after 1100 years of subjugation, wars, annexures and - humiliation. It is again a free country due to the sacrifices made by our - leaders, freedom fighters and has learnt the art of standing tall in the - midst of many a challenge of building the nation with its rich diversity, - cultures, languages all over again since the last 75 years. Unity in - Diversity is our mantra while making our nation stronger in every - sphere.`, + `My Salutations to one and all whom are embodiments of divine love and true self.India i.e. Bharat (that which revels in light of knowledge and wisdom), the land of seekers, enriched by the depth and vastness of diverse sciences and disciplines, is at the cusp of becoming Vishwa Guru (a global teacher) Vikasit Bharat (a developed Nation, a world leader), all over again, after 1100 years of subjugation, annexes, humiliation and wars.The true Bharat culture which is the core of our wisdom, taught us compassion for all living beings and a sense of oneness with all the nature (Vasudeva Kutumbakam). Bharat today is again a free country due to the sacrifices made by our leaders and freedom fighters. Since the last 79 years, we have learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures and languages.`, 'I heartily welcome everyone who visits the website of this institution.', ], more: 'Read more', @@ -2163,8 +2156,28 @@ rolls down to 60% of the eligible students for second round of placement session 'His vision for NIT KKR: He is focused on implementing NEP 2020 in toto at NIT Kurukshetra. He further wants to change the curriculum from outcome based education model into value based education model from the coming academic session 2022-23. The intent is to transform NIT KKR as Takshashila of yesteryears and bringing back India as Vishva Guru and put NIT KKR at the World map as leading educational institute offering holistic personalities to the World and produce leaders from NIT Kurukshetra. We have entered into 60th year of our existence and are upbeat in going for a yearlong celebration.', ], DirectorMessage: [ - 'India, the land of seekers, is at the cusp of becoming Vishwa Guru all over again after 1100 years of subjugation, wars, annexures and humiliation. It is again a free country due to the sacrifices made by our leaders, freedom fighters and has learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures, languages all over again since the last 75 years. Unity in Diversity is our mantra while making our nation stronger in every sphere.', - 'India, the land of seekers, is at the cusp of becoming Vishwa Guru all over again after 1100 years of subjugation, wars, annexures and humiliation. It is again a free country due to the sacrifices made by our leaders, freedom fighters and has learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures, languages all over again since the last 75 years. Unity in Diversity is our mantra while making our nation stronger in every sphere.', + 'MY Salutations to one and all whom are embodiments of divine love and true self.', + 'India i.e. Bharat (that which revels in light of knowledge and wisdom), the land of seekers, enriched by the depth and vastness of diverse sciences and disciplines, is at the cusp of becoming Vishwa Guru (a global teacher) Vikasit Bharat (a developed Nation, a world leader), all over again, after 1100 years of subjugation, annexes, humiliation and wars.', + 'The true Bharat culture which is the core of our wisdom, taught us compassion for all living beings and a sense of oneness with all the nature (Vasudeva Kutumbakam). Bharat today is again a free country due to the sacrifices made by our leaders and freedom fighters. Since the last 79 years, we have learnt the art of standing tall in the midst of many a challenge of building the nation with its rich diversity, cultures and languages.', + '"Unity in Diversity" is our mantra, as we continue to make our Nation stronger in every sphere. Time has now come for us to revisit our rich cultural heritage, and our traditional knowledge and wisdom, bestowed upon us by Rishis, seers, gurus and Acharyas. Over eons, they have blessed us with the Vedas, the foundational scriptures for humanism (written in hymns and rituals), Upanishads (giving the philosophical content, focusing on ultimate reality and the self), the Puranas (collections of genealogies, legends of kings and deities), and the Itihasas (a collection of epic poems like the Mahabharata and Ramayana narrating moral and spiritual themes).', + 'Under the aegis of renowned GuruKul system of school education, adopting distinctive pedagogy, the students all over the world came to Bharat to study in our world renowned universities such as Taxashila, Nalanda, Vikramashila, Valabhi, Odantapuri, Somapuri, Ujjain, Kanchi and Pushpagiri, the great seats of higher learning.', + 'It has been proven without doubt over centuries, that no Nation has ever risen to the stature of a world leader or a happy nation without educating its people. The role of Universities and Centers of Excellence was never in question. Creativity, innovation and hands on experience were given importance, with nature itself serving as the experimental laboratory to unravel the secrets of the universe.', + 'These Universities rose to international repute not merely because of their endless collection of scriptures, but because of the value education that they offered. Their true glory lay in the Knowledge on how best a human being can function in this world. How to use the intelligence that our race possesses in the service of life? Knowledge was transmitted through the famous Guru Shishya Parampara across ages and generations. Students explored a wide range of disciplines, the 64 art forms, through recitation, hands on experience and experiential learning.', + 'This very land of Kurukshetra, also known as Dharma Kshetra, has taught us to be righteous in our conduct, the upholding of values, and the strength to desist any attacks on oneself or upon the vulnerable. The celestial song of Bhagavat Gita teaches us to achieve a 3600 development- Physical, Mental, Emotional, Spiritual and Social wellbeing. It shows us the path to becoming leaders par excellence by imbibing qualities such as adaptability, vision, mindfulness, resilience, focus and gratitude.', + 'The Gita seeks to dispel all our doubts, predicaments, guiding us to explore both the self and the material world outside. A high Spiritual Quotient is a higher dimensional science or intelligence and devotion to God benefits people of every background, age and educational level. Spirituality is a state of awareness; religion is the means to attain that awareness.', + 'Our new National Education Policy 2020, is rooted in this ethos. It seeks to move beyond western educational models imposed upon us, which while showing the path of material progress through scientific innovations and technological advancement, often ignored deeper human values. The NEP-2020 revives our traditional knowledge and wisdom to save the world by two crucial ways:', + 'by developing human excellence, producing individuals who enrich society.', + 'By fostering sustainable technologies for safe guarding Mother Earth and the future of the mankind.', + 'Time has proven, that the unchecked pursuit of material wealth has shaken the very ethos of mankind and endangered the sustenance of the world. (as noted in the document on Sustainable Developmental Goals, SDG-17).', + 'Now is the moment to teach "True / Right Education for knowledge which liberates" (Sa Vidya Ya Vimukthaye), alongside the "Right to Education for all" so that growth and sustenance of mankind may be secured. A fine balance of material growth along with spiritual growth will make the world much more sustainable but also a happy space for all mankind practicing "Value based Educational model" over the current "Outcome based educational model.', + 'So, what should be the right setting for a great nation like India – and for NIT Kurukshetra in particular- to tap into the full potentialities of young minds? These students drawn from across the nation through a rigorous process of selection through national level testing, toil tirelessly to reach these portals of learning. It is our responsibility to provide them the right environment for teaching and learning, and to allow them to explore their self and progress not only in advancing technologies but also promoting their innate skills of creativity and innovation.', + 'These very traits must guide them in solving many a societal problems and set an example that universities and center of excellence are not isolated spaces for exploration of knowledge alone but contributors to the growth of the nation- through setting up of incubation centers, promote start up culture and entrepreneurial mindset.', + 'In this direction, NIT KKR would end the motions of rote learning and changing the setting for critical thinking, enquiry, debate and discussions while promoting experiential learning by connecting these young minds through NIT KKR – Local community link. No education is complete if the scholar is unable to move from levels of learning to achieve knowledge leading to wisdom. If a nation has to become strong and be a role model for others, my children studying at NIT Kurukshetra should aspire to become torch bearers of Bharat innovations, promote sustainable research and develop technologies for the problems of our Nation first and then conquer the world stage. This will solve our twin problems of brain drain and dollar drain and realize the dream of the Nation, to become numero uno by 2047 i.e. Vikasit Bharat.', + 'Having taken over the charge of Director of one of the oldest REC, now transformed as NIT with the status of Institution of National Importance, I along with my teaching, non-teaching faculty and support staff welcome you and are eagerly waiting for all our dear students to come to the campus, leaving no stone unturned in preparing ourselves in welcoming you. Over the last 4 years, many efforts were made to make your stay and your academic journey memorable. As a leader, I assure you that you will be pampered by creating an atmosphere of comfort of a home, spaces much bigger than a home to explore oneself, provide facilities to explore material progress, and self-realization, while allowing you to dream big. I personally wish each one of you become passionate about life and serve the society at large in the form of technocrats, business men, world leaders etc. National Education Policy 2020 (NEP 2020) and its implementation is our top priority and many a steps are taken in this regard by bringing a sea change in curriculum and its contents, besides changing the pedagogy for bringing transformation in you.', + 'I congratulate all student aspirants and their parents to have made efforts in helping them to enter portals of NIT KKR. I Wish all family members and all stake holders of NIT Kurukshetra, the teachers, the students, the parents, alumni and all academic institutions success in their endeavors. The logo of NIT KKR, has a Motto which reads as follows, and I wish you to follow this Mantra', + '"Shramaye Anavarat chesta cha"', + 'which means "Hard work and consistent efforts leads to excellence"', + 'Jai Hind…………. Jai Bharat……………', ], employes: [ { diff --git a/i18n/hi.ts b/i18n/hi.ts index 7436f0216..cf7b6385b 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -141,10 +141,7 @@ const text: Translations = { title: 'निर्देशक का कोना', name: 'डा. बी. वी. रमणा रेड्डी', quote: [ - `भारत, साधकों की भूमि, ११०० वर्षों की पराधीनता, युद्ध, विलय और अपमान के बाद फिर से विश्व गुरु बन्ने - के शिखर पर है। हमारे नेताओं, स्वतंत्रता सेनानियों के बलिदान के कारण ७५ वर्षों से यह फिर से एक स्वतंत्र - देश है और इसने अपनी समृद्ध विविधता, संस्कृतियों, भाषाओं के साथ राष्ट्र के निर्माण की कई चुनौतियों के बीच - खड़े होने की कला सीख ली है। देश को हर क्षेत्र में मजबूत बनाते हुए विविधता में एकता ही हमारा मंत्र है।`, + `मेरा प्रणाम उन सभी को जो दिव्य प्रेम और सच्चे स्वरूप के साकार रूप हैं। भारत अर्थात् भारत (जो ज्ञान और प्रकाश में आनंदित होता है), साधकों की भूमि, विविध विज्ञानों और अनुशासनों की गहराई और विशालता से समृद्ध, 1100 वर्षों की गुलामी, अधिग्रहण, अपमान और युद्धों के बाद फिर से विश्व गुरु (वैश्विक शिक्षक) विकसित भारत (एक विकसित राष्ट्र, विश्व नेता) बनने की कगार पर है। सच्ची भारतीय संस्कृति जो हमारी बुद्धिमत्ता का मूल है, ने हमें सभी जीवों के प्रति करुणा और प्रकृति के साथ एकता की भावना (वसुधैव कुटुम्बकम्) सिखाई है। आज भारत हमारे नेताओं और स्वतंत्रता सेनानियों के बलिदानों के कारण फिर से एक स्वतंत्र देश है। पिछले 79 वर्षों से, हमने अपनी समृद्ध विविधता, संस्कृतियों और भाषाओं के साथ राष्ट्र निर्माण की चुनौतियों के बीच खड़े रहने की कला सीखी है।`, 'मैं इस संस्था की वेबसाइट पर आने वाले सभी लोगों का हृदय से स्वागत करता हूं।', ], more: 'और पढ़ें', @@ -2120,8 +2117,27 @@ const text: Translations = { 'एनआईटी कुरुक्षेत्र के लिए उनका दृष्टिकोण: वे संस्थान में राष्ट्रीय शिक्षा नीति (NEP 2020) को पूर्ण रूप से लागू करने पर केंद्रित हैं। वे 2022-23 के शैक्षणिक सत्र से पाठ्यक्रम को आउटपुट आधारित शिक्षा से मूल्य आधारित शिक्षा मॉडल में बदलने के इच्छुक हैं। उनका उद्देश्य एनआईटी कुरुक्षेत्र को तक्षशिला की भांति विश्व प्रसिद्ध संस्थान बनाना है जो वैश्विक स्तर पर समग्र व्यक्तित्व विकसित कर समाज के लिए नेतृत्वकर्ता तैयार करे। एनआईटी कुरुक्षेत्र अपनी स्थापना के 60वें वर्ष में प्रवेश कर चुका है और वर्षभर चलने वाले उत्सवों की तैयारी में है।', ], DirectorMessage: [ - "भारत, साधकों की भूमि, 1100 वर्षों की गुलामी, युद्धों, अधिग्रहणों और अपमान के बाद फिर से विश्वगुरु बनने की दहलीज पर खड़ा है। यह देश हमारे नेताओं, स्वतंत्रता सेनानियों के बलिदानों के कारण पुनः स्वतंत्र हुआ है और पिछले 75 वर्षों में अपनी विविधता, संस्कृतियों, भाषाओं के साथ एक सशक्त राष्ट्र के रूप में उभरने की कला सीख चुका है। 'विविधता में एकता' हमारा मूल मंत्र है जो हमें हर क्षेत्र में सशक्त बना रहा है।", - "भारत, साधकों की भूमि, 1100 वर्षों की गुलामी, युद्धों, अधिग्रहणों और अपमान के बाद फिर से विश्वगुरु बनने की दहलीज पर खड़ा है। यह देश हमारे नेताओं, स्वतंत्रता सेनानियों के बलिदानों के कारण पुनः स्वतंत्र हुआ है और पिछले 75 वर्षों में अपनी विविधता, संस्कृतियों, भाषाओं के साथ एक सशक्त राष्ट्र के रूप में उभरने की कला सीख चुका है। 'विविधता में एकता' हमारा मूल मंत्र है जो हमें हर क्षेत्र में सशक्त बना रहा है।", + 'मेरा प्रणाम उन सभी को जो दिव्य प्रेम और सच्चे स्वरूप के साकार रूप हैं।', + 'भारत अर्थात् भारत (जो ज्ञान और प्रकाश में आनंदित होता है), साधकों की भूमि, विविध विज्ञानों और अनुशासनों की गहराई और विशालता से समृद्ध, 1100 वर्षों की गुलामी, अधिग्रहण, अपमान और युद्धों के बाद फिर से विश्व गुरु (वैश्विक शिक्षक) विकसित भारत (एक विकसित राष्ट्र, विश्व नेता) बनने की कगार पर है।', + 'सच्ची भारतीय संस्कृति जो हमारी बुद्धिमत्ता का मूल है, ने हमें सभी जीवों के प्रति करुणा और प्रकृति के साथ एकता की भावना (वसुधैव कुटुम्बकम्) सिखाई है। आज भारत हमारे नेताओं और स्वतंत्रता सेनानियों के बलिदानों के कारण फिर से एक स्वतंत्र देश है। पिछले 79 वर्षों से, हमने अपनी समृद्ध विविधता, संस्कृतियों और भाषाओं के साथ राष्ट्र निर्माण की चुनौतियों के बीच खड़े रहने की कला सीखी है।', + '"विविधता में एकता" हमारा मंत्र है, जैसे-जैसे हम अपने राष्ट्र को हर क्षेत्र में मजबूत बना रहे हैं। अब समय आ गया है कि हम अपनी समृद्ध सांस्कृतिक विरासत और हमारे पारंपरिक ज्ञान व बुद्धिमत्ता पर पुनर्विचार करें, जो ऋषियों, द्रष्टाओं, गुरुओं और आचार्यों द्वारा हमें प्रदान की गई है। युगों से, उन्होंने हमें वेदों (मानवतावाद के मूलभूत ग्रंथ), उपनिषदों (दार्शनिक सामग्री प्रदान करने वाले), पुराणों (वंशावली और देवताओं की कथाओं के संग्रह), और इतिहासों (महाभारत और रामायण जैसे महाकाव्यों का संग्रह) से आशीर्वादित किया है।', + 'प्रसिद्ध गुरुकुल प्रणाली के तहत, विशिष्ट शिक्षाशास्त्र अपनाकर, दुनिया भर के छात्र भारत आते थे और हमारे विश्व प्रसिद्ध विश्वविद्यालयों जैसे तक्षशिला, नालंदा, विक्रमशिला, वल्लभी, ओदंतपुरी, सोमपुरी, उज्जैन, कांची और पुष्पगिरि में अध्ययन करते थे।', + 'सदियों से यह सिद्ध हो चुका है कि बिना अपने लोगों को शिक्षित किए कोई भी राष्ट्र विश्व नेता या खुशहाल राष्ट्र का दर्जा नहीं पा सका है। विश्वविद्यालयों और उत्कृष्टता केंद्रों की भूमिका कभी संदेह में नहीं थी। रचनात्मकता, नवाचार और व्यावहारिक अनुभव को महत्व दिया जाता था, प्रकृति स्वयं ब्रह्मांड के रहस्यों को उजागर करने के लिए प्रयोगशाला का काम करती थी।', + 'ये विश्वविद्यालय केवल अंतहीन ग्रंथों के संग्रह के कारण नहीं बल्कि उनके द्वारा प्रदान की जाने वाली मूल्य शिक्षा के कारण अंतर्राष्ट्रीय ख्याति प्राप्त करते थे। उनकी सच्ची महिमा इस ज्ञान में निहित थी कि मनुष्य इस संसार में कैसे सर्वोत्तम रूप से कार्य कर सकता है। जीवन की सेवा में हमारी जाति के पास मौजूद बुद्धिमत्ता का उपयोग कैसे करें? प्रसिद्ध गुरु-शिष्य परंपरा के माध्यम से ज्ञान का स्थानांतरण होता था। छात्र पाठन, व्यावहारिक अनुभव और प्रयोगात्मक शिक्षा के माध्यम से 64 कलाओं सहित व्यापक विषयों का अध्ययन करते थे।', + 'कुरुक्षेत्र की यह भूमि, जिसे धर्म क्षेत्र भी कहा जाता है, ने हमें अपने आचरण में धर्मनिष्ठ होना, मूल्यों को बनाए रखना और स्वयं या कमजोरों पर होने वाले हमलों का विरोध करने की शक्ति सिखाई है। भगवद गीता का दिव्य गीत हमें 360° विकास - शारीरिक, मानसिक, भावनात्मक, आध्यात्मिक और सामाजिक कल्याण प्राप्त करना सिखाता है। यह अनुकूलनशीलता, दृष्टि, सचेतता, लचीलापन, ध्यान और कृतज्ञता जैसे गुणों को अपनाकर उत्कृष्ट नेता बनने का मार्ग दिखाता है।', + 'गीता हमारी सभी शंकाओं और समस्याओं को दूर करने का प्रयास करती है, हमें स्वयं और बाहरी भौतिक संसार दोनों की खोज करने के लिए मार्गदर्शन करती है। उच्च आध्यात्मिक बुद्धिमत्ता एक उच्च आयामी विज्ञान या बुद्धिमत्ता है और ईश्वर के प्रति भक्ति हर पृष्ठभूमि, आयु और शैक्षणिक स्तर के लोगों को लाभ पहुंचाती है। आध्यात्मिकता जागरूकता की अवस्था है; धर्म उस जागरूकता को प्राप्त करने का साधन है।', + 'हमारी नई राष्ट्रीय शिक्षा नीति 2020 इसी भावना में निहित है। यह हम पर थोपे गए पश्चिमी शैक्षिक मॉडल से आगे बढ़ने का प्रयास करती है, जो वैज्ञानिक नवाचारों और तकनीकी प्रगति के माध्यम से भौतिक प्रगति का मार्ग दिखाते हुए अक्सर गहरे मानवीय मूल्यों की उपेक्षा करते थे। NEP-2020 दो महत्वपूर्ण तरीकों से संसार को बचाने के लिए हमारे पारंपरिक ज्ञान और बुद्धिमत्ता को पुनर्जीवित करती है:', + 'मानवीय उत्कृष्टता विकसित करके, ऐसे व्यक्तियों का निर्माण करके जो समाज को समृद्ध बनाते हैं।', + 'मातृ पृथ्वी और मानवजाति के भविष्य की सुरक्षा के लिए टिकाऊ प्रौद्योगिकियों को बढ़ावा देकर।', + 'समय ने सिद्ध कर दिया है कि भौतिक धन की अनियंत्रित खोज ने मानवजाति की मूल भावना को हिला दिया है और संसार के अस्तित्व को खतरे में डाल दिया है।', + 'अब "ज्ञान जो मुक्ति दिलाता है" (सा विद्या या विमुक्तये) के लिए "सच्ची/सही शिक्षा" और "सभी के लिए शिक्षा का अधिकार" सिखाने का क्षण है ताकि मानवजाति की वृद्धि और निर्वाह सुनिश्चित हो सके। आध्यात्मिक विकास के साथ भौतिक विकास का संतुलन दुनिया को अधिक टिकाऊ बनाएगा और "मूल्य आधारित शैक्षिक मॉडल" का अभ्यास करने वाली सभी मानवजाति के लिए एक खुशहाल स्थान भी बनाएगा।', + 'तो, भारत जैसे महान राष्ट्र के लिए - और विशेष रूप से NIT कुरुक्षेत्र के लिए - युवा मनों की पूर्ण क्षमताओं का दोहन करने के लिए सही माहौल क्या होना चाहिए? राष्ट्रीय स्तर की परीक्षा के माध्यम से कठोर चयन प्रक्रिया द्वारा पूरे देश से चुने गए ये छात्र इन शिक्षा के द्वारों तक पहुंचने के लिए अथक परिश्रम करते हैं। उन्हें शिक्षण और सीखने के लिए उचित वातावरण प्रदान करना हमारी जिम्मेदारी है।', + 'इस दिशा में, NIT KKR रटने की प्रणाली को समाप्त करके आलोचनात्मक सोच, पूछताछ, बहस और चर्चा के लिए माहौल बदलेगा। यदि कोई राष्ट्र मजबूत बनना है और दूसरों के लिए आदर्श बनना है, तो NIT कुरुक्षेत्र में पढ़ने वाले मेरे बच्चों को भारतीय नवाचारों के मशालची बनने, टिकाऊ अनुसंधान को बढ़ावा देने और पहले हमारे राष्ट्र की समस्याओं के लिए प्रौद्योगिकियां विकसित करने की आकांक्षा करनी चाहिए।', + 'भारत के सबसे पुराने REC में से एक के निदेशक का पदभार संभालते हुए, जो अब राष्ट्रीय महत्व के संस्थान के रूप में NIT में परिवर्तित हो गया है, मैं अपने शिक्षण, गैर-शिक्षण संकाय और सहायक कर्मचारियों के साथ आपका स्वागत करता हूं। राष्ट्रीय शिक्षा नीति 2020 (NEP 2020) और इसका कार्यान्वयन हमारी सर्वोच्च प्राथमिकता है।', + 'मैं सभी छात्र आकांक्षियों और उनके माता-पिता को बधाई देता हूं कि उन्होंने NIT KKR के द्वारों में प्रवेश पाने में प्रयास किए हैं। मैं NIT कुरुक्षेत्र के सभी परिवारजनों और हितधारकों, शिक्षकों, छात्रों, अभिभावकों, पूर्व छात्रों और सभी शैक्षणिक संस्थानों की सफलता की कामना करता हूं। NIT KKR के लोगो में एक आदर्श वाक्य है, और मैं चाहता हूं कि आप इस मंत्र का पालन करें:', + '"श्रमाये अनवरत चेष्टा च"', + 'जिसका अर्थ है "कड़ी मेहनत और निरंतर प्रयास उत्कृष्टता की ओर ले जाते हैं"', + 'जय हिंद…………. जय भारत……………', ], employes: [ { From 60b227a551128fbd4257b24baf18c926a34fdae7 Mon Sep 17 00:00:00 2001 From: Debatreya Date: Wed, 28 Jan 2026 08:07:50 +0530 Subject: [PATCH 36/73] feat: Website contributors --- .../contributor-card.tsx | 57 +++++++++ .../page.tsx | 108 ++++++++++++++++++ app/[locale]/footer.tsx | 4 + i18n/en.ts | 8 ++ i18n/hi.ts | 8 ++ i18n/translations.ts | 7 ++ server/db/schema/index.ts | 1 + .../db/schema/website-contributors.schema.ts | 24 ++++ 8 files changed, 217 insertions(+) create mode 100644 app/[locale]/contributions-for-website-development/contributor-card.tsx create mode 100644 app/[locale]/contributions-for-website-development/page.tsx create mode 100644 server/db/schema/website-contributors.schema.ts diff --git a/app/[locale]/contributions-for-website-development/contributor-card.tsx b/app/[locale]/contributions-for-website-development/contributor-card.tsx new file mode 100644 index 000000000..84717fda2 --- /dev/null +++ b/app/[locale]/contributions-for-website-development/contributor-card.tsx @@ -0,0 +1,57 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; + +import { cn } from '~/lib/utils'; + +interface ContributorCardProps { + name: string; + rollNumber: string; + image?: string | null; + rollNumberLabel: string; +} + +const FALLBACK_IMAGE = 'fallback/user-image.jpg'; + +export default function ContributorCard({ + name, + rollNumber, + image, + rollNumberLabel, +}: ContributorCardProps) { + const [useFallback, setUseFallback] = useState(false); + + const imageSrc = useFallback || !image ? FALLBACK_IMAGE : image; + + return ( +
      + {/* Contributor Image */} +
      + {name} setUseFallback(true)} + /> +
      + + {/* Contributor Info */} +
      +

      + {name} +

      +

      + {rollNumberLabel}: {rollNumber} +

      +
      +
      + ); +} diff --git a/app/[locale]/contributions-for-website-development/page.tsx b/app/[locale]/contributions-for-website-development/page.tsx new file mode 100644 index 000000000..282c42831 --- /dev/null +++ b/app/[locale]/contributions-for-website-development/page.tsx @@ -0,0 +1,108 @@ +// filepath: /home/uncanny/Desktop/nitkkr/app/[locale]/contributions-for-website-development/page.tsx +// Revalidate every 5 minutes (has DB calls) +export const revalidate = 300; + +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; + +import ContributorCard from './contributor-card'; + +interface Contributor { + id: number; + name: string; + rollNumber: string; + passoutYear: number; + image: string | null; +} + +export default async function ContributionsPage({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const text = (await getTranslations(locale)).WebsiteContributors; + + // Fetch contributors from database + const contributors = await db.query.websiteContributors.findMany({ + columns: { + id: true, + name: true, + rollNumber: true, + passoutYear: true, + image: true, + }, + orderBy: (contributor, { desc, asc }) => [ + desc(contributor.passoutYear), + asc(contributor.name), + ], + }); + + // Group contributors by passout year + const contributorsByYear = contributors.reduce>( + (acc, contributor) => { + const year = contributor.passoutYear; + if (!acc[year]) { + acc[year] = []; + } + acc[year].push(contributor); + return acc; + }, + {} + ); + + // Get sorted years (descending order) + const sortedYears = Object.keys(contributorsByYear) + .map(Number) + .sort((a, b) => b - a); + + return ( + <> + {/* Header Section */} + + + {/* Description Section */} +
      +

      + {text.description} +

      +
      + + {/* Contributors by Year */} +
      + {sortedYears.length === 0 ? ( +

      {text.noContributors}

      + ) : ( + sortedYears.map((year) => ( +
      + {/* Year Heading */} + + + {/* Contributors Grid */} +
      + {contributorsByYear[year].map((contributor) => ( + + ))} +
      +
      + )) + )} +
      + + ); +} diff --git a/app/[locale]/footer.tsx b/app/[locale]/footer.tsx index 1adc4381c..eccfa6197 100644 --- a/app/[locale]/footer.tsx +++ b/app/[locale]/footer.tsx @@ -64,6 +64,10 @@ export default async function Footer({ locale }: { locale: string }) { href: '/institute/sections/library/membership-and-privileges', }, { name: 'Research Scholars', href: '/faculty-and-staff?tab=scholars' }, + { + name: 'Contributions for Website Development', + href: '/contributions-for-website-development', + }, ]; return ( diff --git a/i18n/en.ts b/i18n/en.ts index d7a9b7db1..a63ba7f72 100644 --- a/i18n/en.ts +++ b/i18n/en.ts @@ -2360,6 +2360,14 @@ rolls down to 60% of the eligible students for second round of placement session ], }, }, + WebsiteContributors: { + pageTitle: 'Contributions for Website Development', + description: + 'We extend our heartfelt gratitude to all the students who have contributed to the development and maintenance of the NIT Kurukshetra website. Their dedication, technical expertise, and creative vision have been instrumental in building this digital platform.', + passoutYear: 'Passout Year', + rollNumber: 'Roll No.', + noContributors: 'No contributors found for this year.', + }, }; export default text; diff --git a/i18n/hi.ts b/i18n/hi.ts index cf7b6385b..ff5dae8c9 100644 --- a/i18n/hi.ts +++ b/i18n/hi.ts @@ -2330,6 +2330,14 @@ const text: Translations = { ], }, }, + WebsiteContributors: { + pageTitle: 'वेबसाइट विकास में योगदान', + description: + 'हम उन सभी छात्रों के प्रति हार्दिक आभार व्यक्त करते हैं जिन्होंने एनआईटी कुरुक्षेत्र की वेबसाइट के विकास और रखरखाव में योगदान दिया है। उनका समर्पण, तकनीकी विशेषज्ञता और रचनात्मक दृष्टि इस डिजिटल प्लेटफॉर्म के निर्माण में महत्वपूर्ण रही है।', + passoutYear: 'स्नातक वर्ष', + rollNumber: 'रोल नंबर', + noContributors: 'इस वर्ष के लिए कोई योगदानकर्ता नहीं मिला।', + }, }; export default text; diff --git a/i18n/translations.ts b/i18n/translations.ts index cdbfac156..d34f7f563 100644 --- a/i18n/translations.ts +++ b/i18n/translations.ts @@ -1393,4 +1393,11 @@ export interface Translations { list: string[]; }; }; + WebsiteContributors: { + pageTitle: string; + description: string; + passoutYear: string; + rollNumber: string; + noContributors: string; + }; } diff --git a/server/db/schema/index.ts b/server/db/schema/index.ts index 78cfb59f2..b07b9cb8a 100644 --- a/server/db/schema/index.ts +++ b/server/db/schema/index.ts @@ -47,3 +47,4 @@ export * from './other-officers.schema'; export * from './senate-composition.schema'; export * from './senate-agenda-minutes.schema'; export * from './scsa_minutes.schema'; +export * from './website-contributors.schema'; diff --git a/server/db/schema/website-contributors.schema.ts b/server/db/schema/website-contributors.schema.ts new file mode 100644 index 000000000..11a7883d3 --- /dev/null +++ b/server/db/schema/website-contributors.schema.ts @@ -0,0 +1,24 @@ +import { relations } from 'drizzle-orm'; +import { pgTable } from 'drizzle-orm/pg-core'; + +import { students } from '.'; + +export const websiteContributors = pgTable('website_contributors', (t) => ({ + id: t.serial().primaryKey(), + name: t.varchar({ length: 256 }).notNull(), + rollNumber: t.varchar({ length: 20 }).notNull(), + passoutYear: t.integer().notNull(), + image: t.varchar({ length: 512 }), + studentId: t.integer().references(() => students.id), + createdAt: t.timestamp().defaultNow().notNull(), +})); + +export const websiteContributorsRelations = relations( + websiteContributors, + ({ one }) => ({ + student: one(students, { + fields: [websiteContributors.studentId], + references: [students.id], + }), + }) +); From 7762876e0f948ba96cf72caa4135c8416cf8fca4 Mon Sep 17 00:00:00 2001 From: Debatreya Date: Wed, 28 Jan 2026 13:51:40 +0530 Subject: [PATCH 37/73] fix: Schema update contributors --- server/db/schema/website-contributors.schema.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/db/schema/website-contributors.schema.ts b/server/db/schema/website-contributors.schema.ts index 11a7883d3..2e49aa98f 100644 --- a/server/db/schema/website-contributors.schema.ts +++ b/server/db/schema/website-contributors.schema.ts @@ -10,6 +10,9 @@ export const websiteContributors = pgTable('website_contributors', (t) => ({ passoutYear: t.integer().notNull(), image: t.varchar({ length: 512 }), studentId: t.integer().references(() => students.id), + linkedinId: t.varchar({ length: 512 }), + githubId: t.varchar({ length: 512 }), + designation: t.varchar({ enum: ['developer', 'designer', 'devops'] }), createdAt: t.timestamp().defaultNow().notNull(), })); From aa76a5ab47318c36ab3332351aa68aef33731b42 Mon Sep 17 00:00:00 2001 From: Debatreya Das <116421305+Debatreya@users.noreply.github.com> Date: Fri, 30 Jan 2026 00:04:18 +0530 Subject: [PATCH 38/73] feat: CCN Login and Notifications CRUD (#512) This pull request introduces a complete CRUD (Create, Read, Update, Delete) workflow for notifications, including UI components for adding, editing, and deleting notifications, as well as authorization checks to ensure only authorized users can manage notifications. It also adds a reusable `NotificationForm` component with support for file uploads and category selection, and updates the notifications list to support edit/delete actions for authorized users. **Key changes:** ### Notification Management Features * Added `NotificationForm` component (`app/[locale]/notifications/NotificationForm.tsx`) with support for creating and editing notifications, category selection, file uploads, and form validation. ([app/[locale]/notifications/NotificationForm.tsxR1-R357](diffhunk://#diff-c940317b11d909c14bca1fe605975dbd148bb037febf01178262b735d6686e85R1-R357)) * Implemented add and edit notification pages and modals (`app/[locale]/notifications/add/page.tsx`, `app/[locale]/notifications/edit/[id]/page.tsx`, `app/[locale]/@modals/(.)notifications/add/page.tsx`, `app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx`) with authorization checks and pre-filled form data for editing. ([app/[locale]/notifications/add/page.tsxR1-R53](diffhunk://#diff-f3ca8b1b3454e18173acad18d050c426ff014801f4621b24425adc5853ec8e1dR1-R53), [app/[locale]/notifications/edit/[id]/page.tsxR1-R73](diffhunk://#diff-46f0f24d73a5a7eaa9bd8f591698c08374f4a840f0b6145058c5dad821bcfbf4R1-R73), [app/[locale]/@modals/(.)notifications/add/page.tsxR1-R50](diffhunk://#diff-87c70c87f8bc082341a7a318433c77c6f9b15eefe8eae0d3bda5e21e3d38dd3dR1-R50), [app/[locale]/@modals/(.)notifications/edit/[id]/page.tsxR1-R70](diffhunk://#diff-8a19de32099c182d7458fa9bac7df84c65fd5eab53f8dfb2ba3c9f122fdcbc04R1-R70)) ### Authorization and Routing * Added server-side authorization using `canManageNotifications` and `getServerAuthSession` to restrict access to notification management pages and modals. Unauthorized users are redirected to the notifications list. ([app/[locale]/notifications/add/page.tsxR1-R53](diffhunk://#diff-f3ca8b1b3454e18173acad18d050c426ff014801f4621b24425adc5853ec8e1dR1-R53), [app/[locale]/notifications/edit/[id]/page.tsxR1-R73](diffhunk://#diff-46f0f24d73a5a7eaa9bd8f591698c08374f4a840f0b6145058c5dad821bcfbf4R1-R73), [app/[locale]/@modals/(.)notifications/add/page.tsxR1-R50](diffhunk://#diff-87c70c87f8bc082341a7a318433c77c6f9b15eefe8eae0d3bda5e21e3d38dd3dR1-R50), [app/[locale]/@modals/(.)notifications/edit/[id]/page.tsxR1-R70](diffhunk://#diff-8a19de32099c182d7458fa9bac7df84c65fd5eab53f8dfb2ba3c9f122fdcbc04R1-R70)) * Updated notification section link to use locale-aware routing. ([app/[locale]/notifications.tsxL38-R38](diffhunk://#diff-fc4ab4f58418a1ce1ca032d258b139c979709ec0e41edf5494c8c0b99a31b6ecL38-R38)) ### Notifications List Enhancements * Updated `NotificationsList` component (`app/[locale]/notifications/NotificationsList.tsx`) to support edit and delete actions for users with management permissions, including UI buttons and optimistic UI updates on deletion. ([app/[locale]/notifications/NotificationsList.tsxR3-R11](diffhunk://#diff-922bf71c74514dbedab3a7c50a7cb793d1a91ec714bc5fa269eec73c587380faR3-R11), [app/[locale]/notifications/NotificationsList.tsxR25-R31](diffhunk://#diff-922bf71c74514dbedab3a7c50a7cb793d1a91ec714bc5fa269eec73c587380faR25-R31), [app/[locale]/notifications/NotificationsList.tsxR41-R49](diffhunk://#diff-922bf71c74514dbedab3a7c50a7cb793d1a91ec714bc5fa269eec73c587380faR41-R49), [app/[locale]/notifications/NotificationsList.tsxR61-R79](diffhunk://#diff-922bf71c74514dbedab3a7c50a7cb793d1a91ec714bc5fa269eec73c587380faR61-R79), [app/[locale]/notifications/NotificationsList.tsxL120-R179](diffhunk://#diff-922bf71c74514dbedab3a7c50a7cb793d1a91ec714bc5fa269eec73c587380faL120-R179)) These changes collectively enable a robust and user-friendly notification management experience, gated by proper authorization, with a modern UI for both general and privileged users. --- .../@modals/(.)notifications/add/page.tsx | 50 +++ .../(.)notifications/edit/[id]/page.tsx | 70 ++++ app/[locale]/notifications.tsx | 2 +- .../notifications/NotificationForm.tsx | 357 ++++++++++++++++++ .../notifications/NotificationsList.tsx | 56 ++- app/[locale]/notifications/add/page.tsx | 53 +++ app/[locale]/notifications/edit/[id]/page.tsx | 73 ++++ app/[locale]/notifications/page.tsx | 22 ++ app/[locale]/profile/SectionProfile.tsx | 83 ++++ app/[locale]/profile/layout.tsx | 12 +- app/[locale]/profile/page.tsx | 24 +- i18n/en.ts | 18 +- i18n/hi.ts | 15 + i18n/translations.ts | 16 + server/actions/notifications.ts | 260 ++++++++++++- server/auth.ts | 105 +++++- 16 files changed, 1199 insertions(+), 17 deletions(-) create mode 100644 app/[locale]/@modals/(.)notifications/add/page.tsx create mode 100644 app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx create mode 100644 app/[locale]/notifications/NotificationForm.tsx create mode 100644 app/[locale]/notifications/add/page.tsx create mode 100644 app/[locale]/notifications/edit/[id]/page.tsx create mode 100644 app/[locale]/profile/SectionProfile.tsx diff --git a/app/[locale]/@modals/(.)notifications/add/page.tsx b/app/[locale]/@modals/(.)notifications/add/page.tsx new file mode 100644 index 000000000..78fba7f24 --- /dev/null +++ b/app/[locale]/@modals/(.)notifications/add/page.tsx @@ -0,0 +1,50 @@ +import { redirect } from 'next/navigation'; + +import { Dialog } from '~/components/dialog'; +import { Card, CardHeader } from '~/components/ui'; +import { getTranslations } from '~/i18n/translations'; +import { canManageNotifications, getServerAuthSession } from '~/server/auth'; + +import { NotificationForm } from '../../../notifications/NotificationForm'; + +export default async function AddNotificationModal({ + params: { locale }, +}: { + params: { locale: string }; +}) { + // Check authorization + const session = await getServerAuthSession(); + if (!canManageNotifications(session)) { + redirect(`/${locale}/notifications`); + } + + const text = (await getTranslations(locale)).Notifications; + + return ( + + + +

      + {text.addNotification} +

      +
      +
      + +
      +
      +
      + ); +} diff --git a/app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx b/app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx new file mode 100644 index 000000000..cfd70f0c9 --- /dev/null +++ b/app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx @@ -0,0 +1,70 @@ +import { notFound, redirect } from 'next/navigation'; + +import { Dialog } from '~/components/dialog'; +import { Card, CardHeader } from '~/components/ui'; +import { getTranslations } from '~/i18n/translations'; +import { canManageNotifications, getServerAuthSession } from '~/server/auth'; +import { getNotificationForEdit } from '~/server/actions/notifications'; + +import { NotificationForm } from '../../../../notifications/NotificationForm'; + +export default async function EditNotificationModal({ + params: { locale, id }, +}: { + params: { locale: string; id: string }; +}) { + // Check authorization + const session = await getServerAuthSession(); + if (!canManageNotifications(session)) { + redirect(`/${locale}/notifications`); + } + + const notificationId = parseInt(id, 10); + if (isNaN(notificationId)) { + notFound(); + } + + // Fetch notification data + const notification = await getNotificationForEdit(notificationId); + if (!notification) { + notFound(); + } + + const text = (await getTranslations(locale)).Notifications; + + return ( + + + +

      + {text.editNotification} +

      +
      +
      + +
      +
      +
      + ); +} diff --git a/app/[locale]/notifications.tsx b/app/[locale]/notifications.tsx index 29beb0315..16ff8cfe3 100644 --- a/app/[locale]/notifications.tsx +++ b/app/[locale]/notifications.tsx @@ -35,7 +35,7 @@ export default async function Notifications({ className="container" glyphDirection="rtl" heading="h2" - href="#notifications" + href={`/${locale}/notifications`} text={text.title} /> diff --git a/app/[locale]/notifications/NotificationForm.tsx b/app/[locale]/notifications/NotificationForm.tsx new file mode 100644 index 000000000..2ec23371b --- /dev/null +++ b/app/[locale]/notifications/NotificationForm.tsx @@ -0,0 +1,357 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useRef, useState } from 'react'; +import { AiOutlineLoading3Quarters } from 'react-icons/ai'; +import { FaTrash, FaUpload } from 'react-icons/fa'; + +import { Button } from '~/components/buttons'; +import { Input, Textarea } from '~/components/inputs'; +import { notificationCategoryEnum } from '~/server/db/schema/notifications.schema'; +import { + addNotification, + type NotificationFormData, + updateNotification, +} from '~/server/actions/notifications'; +import { uploadMedia } from '~/server/actions/media-upload'; + +type Category = (typeof notificationCategoryEnum.enumValues)[number]; + +interface NotificationFormProps { + locale: string; + /** If provided, the form is in edit mode */ + notificationId?: number; + /** Initial data for editing */ + initialData?: { + title: string; + content: string | null; + categories: string[]; + documents?: string[]; + createdAt?: string; + }; + text: { + notificationTitle: string; + notificationContent: string; + notificationCategories: string; + notificationDate: string; + documents: string; + uploadDocument: string; + save: string; + cancel: string; + categories: Record; + }; +} + +export function NotificationForm({ + locale, + notificationId, + initialData, + text, +}: NotificationFormProps) { + const router = useRouter(); + const isEditing = !!notificationId; + const fileInputRef = useRef(null); + + const [title, setTitle] = useState(initialData?.title ?? ''); + const [content, setContent] = useState(initialData?.content ?? ''); + const [categories, setCategories] = useState( + (initialData?.categories as Category[]) ?? [] + ); + const [notificationDate, setNotificationDate] = useState(() => { + if (initialData?.createdAt) { + return initialData.createdAt.split('T')[0]; // Get date part only + } + return new Date().toISOString().split('T')[0]; + }); + const [documents, setDocuments] = useState( + initialData?.documents ?? [] + ); + const [isSubmitting, setIsSubmitting] = useState(false); + const [isUploading, setIsUploading] = useState(false); + const [error, setError] = useState(null); + + const handleCategoryToggle = (category: Category) => { + setCategories((prev) => + prev.includes(category) + ? prev.filter((c) => c !== category) + : [...prev, category] + ); + }; + + const handleFileUpload = async (e: React.ChangeEvent) => { + const files = e.target.files; + if (!files || files.length === 0) return; + + // Require at least one category before uploading + if (categories.length === 0) { + setError( + 'Please select at least one category before uploading documents' + ); + return; + } + + setIsUploading(true); + setError(null); + + try { + for (const file of Array.from(files)) { + const formData = new FormData(); + formData.append('file', file); + + // Generate path: notifications/{category}/{date}/{filename} + // Use first category and current notification date + const category = categories[0]; + const dateForPath = notificationDate.replace(/-/g, '/'); // Convert YYYY-MM-DD to YYYY/MM/DD + const sanitizedName = file.name.replace(/[^a-zA-Z0-9.-]/g, '_'); + const timestamp = Date.now(); + const s3Path = `isaac-s3-images/notifications/${dateForPath}/${category}/${timestamp}-${sanitizedName}`; + + const result = await uploadMedia(formData, s3Path); + + if (result.success && result.url) { + setDocuments((prev) => [...prev, result.url]); + } else { + setError(result.message); + } + } + } catch (err) { + console.error('Failed to upload file:', err); + setError('Failed to upload file'); + } finally { + setIsUploading(false); + // Reset file input + if (fileInputRef.current) { + fileInputRef.current.value = ''; + } + } + }; + + const handleRemoveDocument = (index: number) => { + setDocuments((prev) => prev.filter((_, i) => i !== index)); + }; + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + setIsSubmitting(true); + + const data: NotificationFormData = { + title: title.trim(), + content: content.trim() || undefined, + categories, + notificationDate, + documents, + }; + + try { + const result = isEditing + ? await updateNotification(notificationId, data) + : await addNotification(data); + + if (result.success) { + router.back(); + router.refresh(); + } else { + setError(result.message); + } + } catch (err) { + console.error('Failed to save notification:', err); + setError('An unexpected error occurred'); + } finally { + setIsSubmitting(false); + } + }; + + const handleCancel = () => { + router.back(); + }; + + // Get all available categories + const allCategories = notificationCategoryEnum.enumValues; + + return ( +
      + {error && ( +
      + {error} +
      + )} + + {/* Title */} +
      + + setTitle(e.target.value)} + required + maxLength={256} + placeholder="Enter notification title" + className="w-full" + /> +
      + + {/* Date */} +
      + + setNotificationDate(e.target.value)} + required + className="w-full" + /> +
      + + {/* Content */} +
      + +