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..05edb4414 --- /dev/null +++ b/app/[locale]/(animations)/animate-footer.tsx @@ -0,0 +1,223 @@ +'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; target?: string }[]; + locale: string; + className?: string; +} + +export function AnimateFooterLinkColumn({ + title, + links, + locale, + className, +}: FooterLinkColumnProps) { + return ( + + + {title} + + + {links.map((item, index) => ( + + {item.target === '_blank' || item.href.startsWith('http') ? ( + + {item.name} + + ) : ( + + {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..c6c53d0dd --- /dev/null +++ b/app/[locale]/(animations)/animate-page-content.tsx @@ -0,0 +1,114 @@ +'use client'; + +import { motion } from 'framer-motion'; +import { TbBrain, TbRocket, TbSchool } from 'react-icons/tb'; +import { HiOutlineAcademicCap } from 'react-icons/hi2'; +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: { + chpd: string; + racs: string; + scoe: string; + thoughtLab: 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]/@modals/(.)notifications/add/page.tsx b/app/[locale]/@modals/(.)notifications/add/page.tsx new file mode 100644 index 000000000..e5bdaa398 --- /dev/null +++ b/app/[locale]/@modals/(.)notifications/add/page.tsx @@ -0,0 +1,52 @@ +import { redirect } from 'next/navigation'; + +import { Dialog } from '~/components/dialog'; +import { Card, CardHeader, ScrollArea } 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..03d9d57d2 --- /dev/null +++ b/app/[locale]/@modals/(.)notifications/edit/[id]/page.tsx @@ -0,0 +1,73 @@ +import { notFound, redirect } from 'next/navigation'; + +import { Dialog } from '~/components/dialog'; +import { Card, CardHeader, ScrollArea } 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]/@modals/(.)profile/edit/client-utils.tsx b/app/[locale]/@modals/(.)profile/edit/client-utils.tsx index b2551976f..a6e5a2db6 100644 --- a/app/[locale]/@modals/(.)profile/edit/client-utils.tsx +++ b/app/[locale]/@modals/(.)profile/edit/client-utils.tsx @@ -13,7 +13,7 @@ import { SelectTrigger, SelectValue, } from '~/components/inputs/select'; -import { CardContent, CardFooter } from '~/components/ui'; +import { CardContent, CardFooter, ScrollArea } from '~/components/ui'; import { FormControl, FormDescription, @@ -346,15 +346,23 @@ export function FacultyPersonalDetailsForm({ return ( -
- - {renderFields( - form.control, - facultyPersonalDetailsSchema.shape, - 'personalDetails' - )} - - + + {/* Scrollable form fields */} + + + {renderFields( + form.control, + facultyPersonalDetailsSchema.shape, + 'personalDetails' + )} + + + + {/* Fixed Footer */} + + + ); +} diff --git a/app/[locale]/@modals/(.)profile/edit/page.tsx b/app/[locale]/@modals/(.)profile/edit/page.tsx index e390976b8..107863a62 100644 --- a/app/[locale]/@modals/(.)profile/edit/page.tsx +++ b/app/[locale]/@modals/(.)profile/edit/page.tsx @@ -2,7 +2,7 @@ import { and, eq, getTableColumns } from 'drizzle-orm'; import { notFound } from 'next/navigation'; import { Dialog } from '~/components/dialog'; -import { Card, CardHeader } from '~/components/ui'; +import { Card, CardHeader, ScrollArea } from '~/components/ui'; import { facultyProfileSchemas } from '~/lib/schemas/faculty-profile'; import { cn, formatCamelCase } from '~/lib/utils'; import { getServerAuthSession } from '~/server/auth'; @@ -22,6 +22,7 @@ import { } from '~/server/db'; import { FacultyForm, FacultyPersonalDetailsForm } from './client-utils'; +import { FacultyPhotoUpload } from './faculty-photo-upload'; const facultyTables = { qualifications, @@ -51,7 +52,10 @@ export default async function Page({ .findFirst({ where: (faculty, { eq }) => eq(faculty.id, userId), columns: { + id: true, + employeeId: true, officeAddress: true, + orcidId: true, scopusId: true, linkedInId: true, googleScholarId: true, @@ -61,10 +65,12 @@ export default async function Page({ with: { person: { columns: { + name: true, countryCode: true, telephone: true, alternateCountryCode: true, alternateTelephone: true, + img: true, }, }, }, @@ -72,7 +78,12 @@ export default async function Page({ .then((result) => { if (!result) return null; return { + id: result.id, + employeeId: result.employeeId, + name: result.person.name, + img: result.person.img ?? undefined, officeAddress: result.officeAddress, + orcidId: result.orcidId ?? undefined, scopusId: result.scopusId ?? undefined, linkedInId: result.linkedInId ?? undefined, googleScholarId: result.googleScholarId ?? undefined, @@ -91,11 +102,29 @@ export default async function Page({ - - + + {/* Fixed Header */} + +

+ Edit Personal Details +

+
+ + {/* Fixed Photo Upload Section */} +
+ +
+ + {/* Scrollable Form Content + Fixed Footer inside the form */} ({ + courseName: course, + })); + + return ( + <> + + {/* ADMISSION */} +
+
+ {text.admission.process.content.map((paragraph, index) => ( +

+ {paragraph} +

+ ))} +
+
+ {/* Vision & Mission */} +
+
+ {/* 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 */} +
+ {text.VisionMissionImage.alt} +
+
+
+ {/* notifications */} +
+ + +
+ {/* HEAD OF CHPD */} +
+ + +
+ {/* features */} +
+ +
+
+
+
+
    + {text.Features.items.map((item, index) => ( +
  • + bullet + + {item} + +
  • + ))} +
+
+
+
+
+
+ {/* Courses */} +
+ +
+ + {/* how to apply */} +
+ +
+
+
+
+
    + {text.How_to_Apply.registrationSteps.map((item, index) => ( +
  1. {item}
  2. + ))} +
+
+
+
+
+
+ {/* for queries */} +
+ + +
+
+ {/* Email */} +
+ + + +
+ + {/* Phone */} +
+ + + +
+
+
+
+ + ); +} 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]/RACS/page.tsx b/app/[locale]/RACS/page.tsx new file mode 100644 index 000000000..312597d30 --- /dev/null +++ b/app/[locale]/RACS/page.tsx @@ -0,0 +1,245 @@ +import Image from 'next/image'; +import Link from 'next/link'; +import { TbMail } from 'react-icons/tb'; + +import { getTranslations } from '~/i18n/translations'; +import Heading from '~/components/heading'; +import ImageHeader from '~/components/image-header'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; +import FICGroup from '~/components/fic-group'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '~/components/ui'; + +export default async function RACS({ + params: { locale }, +}: { + params: { locale: string }; +}) { + const text = (await getTranslations(locale)).RACS; + + const researchProposalForms = [ + { + href: 'https://isac-nitkkr-public.s3.ap-south-1.amazonaws.com/isaac-s3-images/rac-s/Application-for-Grant-of-Funds.pdf', + }, + { + href: 'https://isac-nitkkr-public.s3.ap-south-1.amazonaws.com/isaac-s3-images/rac-s/Form_A.pdf', + }, + { + href: 'https://isac-nitkkr-public.s3.ap-south-1.amazonaws.com/isaac-s3-images/rac-s/Form_B.pdf', + }, + { + href: 'https://isac-nitkkr-public.s3.ap-south-1.amazonaws.com/isaac-s3-images/rac-s/Form_C_terms_conditions.pdf', + }, + { + href: 'https://isac-nitkkr-public.s3.ap-south-1.amazonaws.com/isaac-s3-images/rac-s/Research_Areas_of_SAC_March_2023.pdf', + }, + ]; + + return ( + <> + + {/* INTRO – full width */} +
+
+

+ {text.intro} +

+
+
+ +
+ {/* NOTIFICATIONS */} +
+ + +
+ {/* REGIONAL COORDINATOR */} +
+
+ + +
+
+ {/* RESEARCH PROPOSAL FORMS */} +
+ + +
+ + + + + {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]/academics/admission/page.tsx b/app/[locale]/academics/admission/page.tsx new file mode 100644 index 000000000..bf5c70400 --- /dev/null +++ b/app/[locale]/academics/admission/page.tsx @@ -0,0 +1,278 @@ +import React from 'react'; +import Link from 'next/link'; +import { arrayOverlaps, desc, eq, inArray } from 'drizzle-orm'; + +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; +import { cn } from '~/lib/utils'; +import { buildHref, parseDate, toArray } from '~/lib/helpers'; +import ImageHeader from '~/components/image-header'; +import { Button } from '~/components/buttons'; +import { ScrollArea } from '~/components/ui'; +import { notificationDepartments } from '~/server/db/schema/notifications.schema'; +import { type NotificationItem } from '~/server/actions/notifications'; +import { + DateRangeFilter, + MultiCheckbox, + SearchInput, +} from '~/components/inputs'; +import { MobileFilters } from '~/components/mobile-filters'; +import { FilterSection } from '~/components/filter-section'; + +import { NotificationsList } from '../../notifications/NotificationsList'; + +const DEGREE_LEVELS = ['ug', 'pg', 'phd'] as const; + +const INITIAL_BATCH_SIZE = 20; + +interface PageSearchParams { + q?: string; + department?: string | string[]; + degreeLevel?: string | string[]; + start?: string; + end?: string; +} + +export default async function AdmissionPage({ + params: { locale }, + searchParams, +}: { + params: { locale: string }; + searchParams: PageSearchParams; +}) { + const text = (await getTranslations(locale)).Admission; + + // Normalize multi-select params + const departments = toArray(searchParams.department).filter(Boolean); + const degreeLevels = toArray(searchParams.degreeLevel).filter(Boolean); + const startDate = parseDate(searchParams.start); + const endDate = parseDate(searchParams.end); + const query = (searchParams.q ?? '').trim().toLowerCase(); + + // Fetch departments 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) + : []; + + // 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 (filteredNotificationIds.length === 0) { + filteredNotificationIds = [-1]; // Use impossible ID to return no results + } + } + + // Build base query - always filter by 'admission' category + let raw = await db.query.notifications.findMany({ + where: (n, { and, gte, lte }) => + and( + // Always filter by admission category + arrayOverlaps(n.categories, ['admission']), + startDate ? gte(n.createdAt, startDate) : undefined, + endDate ? lte(n.createdAt, endDate) : undefined, + filteredNotificationIds + ? inArray(n.id, filteredNotificationIds) + : undefined, + // Degree level (educationType) filter — single value at DB level + degreeLevels.length === 1 + ? eq(n.educationType, degreeLevels[0] as 'ug' | 'pg' | 'phd') + : undefined + ), + orderBy: (n) => [desc(n.createdAt)], + limit: INITIAL_BATCH_SIZE + 1, + }); + + // For multiple degree levels, filter in memory + if (degreeLevels.length > 1) { + raw = raw.filter( + (n) => n.educationType && degreeLevels.includes(n.educationType) + ); + } + + // 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, + categories: n.categories, + createdAt: n.createdAt.toISOString(), + })); + + // Build filter params for the client component (for infinite scroll) + const filterParams = { + categories: ['admission'] as string[], + departmentIds: deptIds.length ? deptIds : undefined, + educationType: degreeLevels.length === 1 ? degreeLevels[0] : 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 */} +
+ +
+
+ + {/* Admissions List */} +
+ +
+
+
+ + ); +} diff --git a/app/[locale]/academics/awards/page.tsx b/app/[locale]/academics/awards/page.tsx index c3f54e620..1a0c0282a 100644 --- a/app/[locale]/academics/awards/page.tsx +++ b/app/[locale]/academics/awards/page.tsx @@ -88,7 +88,7 @@ function AwardsCard({ )} >

{text.about}

-

{about}

+

{about}

{children && ( <>

{text.criterion}

@@ -98,7 +98,9 @@ function AwardsCard({ {description && ( <>

{text.criterion}

-

{description}

+

+ {description} +

)} diff --git a/app/[locale]/academics/curricula/[code]/page.tsx b/app/[locale]/academics/curricula/[code]/page.tsx index eece6b7f4..f66f1998d 100644 --- a/app/[locale]/academics/curricula/[code]/page.tsx +++ b/app/[locale]/academics/curricula/[code]/page.tsx @@ -26,22 +26,10 @@ export default async function Curriculum({ }: { params: { locale: string; code: string }; }) { + const codeDecoded = decodeURIComponent(code); const text = (await getTranslations(locale)).Curriculum; const course = await db.query.courses.findFirst({ - where: (course, { eq }) => eq(course.code, code), - with: { - coordinator: { - with: { - person: { - columns: { - name: true, - telephone: true, - email: true, - }, - }, - }, - }, - }, + where: (courses, { eq }) => eq(courses.code, codeDecoded), }); if (!course) notFound(); @@ -95,37 +83,6 @@ export default async function Curriculum({ ))} - -
@@ -183,9 +140,7 @@ export default async function Curriculum({
    {course.essentialReading.map((book, index) => (
  1. -

    - {book} -

    +

    {book}

  2. ))}
diff --git a/app/[locale]/academics/curricula/page.tsx b/app/[locale]/academics/curricula/page.tsx index 8c010c20f..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,33 +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: { @@ -87,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/departments/[name]/page.tsx b/app/[locale]/academics/departments/[name]/page.tsx index 6725b7d30..8fbae0847 100644 --- a/app/[locale]/academics/departments/[name]/page.tsx +++ b/app/[locale]/academics/departments/[name]/page.tsx @@ -9,25 +9,16 @@ import { HiMiniBeaker } from 'react-icons/hi2'; import { MdBadge, MdEmail } from 'react-icons/md'; import { Button } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; import { GalleryCarousel } from '~/components/carousels'; import Heading from '~/components/heading'; import ImageHeader from '~/components/image-header'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; import { db, departments } from '~/server/db'; import { countChildren } from '~/server/s3'; -const hodProfile = { - name: 'Jitender Kumar Chhabra', - designation: 'Professor & Head of Department', - email: 'jk.chhabra@nitkkr.ac.in', - phone: '+91-1744-233-488', - message: [ - 'Welcome to the Department of Computer Engineering at NIT Kurukshetra. Our department has been at the forefront of computer science education and research since its inception, consistently producing industry-ready professionals and innovative researchers.', - 'We are committed to excellence in teaching, research, and innovation. Our state-of-the-art laboratories, experienced faculty, and strong industry connections provide students with the perfect environment for learning and growth.', - ], -}; - export async function generateStaticParams() { return await db.select({ name: departments.urlName }).from(departments); } @@ -41,28 +32,47 @@ export default async function Department({ const department = await db.query.departments.findFirst({ where: (departments, { eq }) => eq(departments.urlName, name), + columns: { + id: true, + name: true, + alias: true, + urlName: true, + about: true, + vision: true, + mission: true, + }, with: { majors: { columns: { degree: true, name: true } } }, }); if (!department) notFound(); // FIXME: Remove this once dynamicParams works const imageCount = await countChildren(`departments/${name}/images`); - - const departmentHead = await db.query.departmentHeads.findFirst({ + const allHeads = await db.query.departmentHeads.findMany({ where: (departmentHead, { eq }) => - eq(departmentHead.departmentId, department.id) && - eq(departmentHead.isActive, true), + eq(departmentHead.departmentId, department.id), with: { faculty: { - columns: { employeeId: true, officeAddress: true }, + columns: { designation: true, employeeId: true, officeAddress: true }, with: { person: { - columns: { email: true, id: true, name: true, telephone: true }, + columns: { name: true, email: true, telephone: true, img: true }, }, }, }, }, }); + const departmentHead = allHeads.find((head) => head.isActive) ?? null; + const hodName = departmentHead?.faculty?.person?.name ?? 'Head of Department'; + const hodDesignation = + departmentHead?.faculty?.designation ?? 'Head of Department'; + const hodEmail = departmentHead?.faculty?.person?.email ?? ''; + const hodPhone = departmentHead?.faculty?.person?.telephone ?? ''; + const hodImg = + departmentHead?.faculty?.person?.img ?? '/placeholder-person.jpg'; + const hodMessage = [ + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed euismod, nunc ut laoreet dictum, urna erat dictum erat, at cursus enim sapien eget urna.', + 'Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Integer ac sem nec urna cursus faucibus.', + ]; return ( <> {department.about} @@ -124,7 +134,7 @@ export default async function Department({ heading="h3" text={text.headings.vision.toUpperCase()} /> -

{department.vision}

+

{department.vision}

-

{department.mission}

+

{department.mission}

+ {/* Notifications Panel for Department */} + {department && ( + + )} +
{hodProfile.name}

- {hodProfile.name} + {hodName}

-

{hodProfile.designation}

+

{hodDesignation}

- {hodProfile.message.map((paragraph, index) => ( + {hodMessage.map((paragraph, index) => (

{paragraph}

))}
- - - {hodProfile.email} - - - - {hodProfile.phone} - + {hodEmail && ( + + + {hodEmail} + + )} + {hodPhone && ( + + + {hodPhone} + + )}
@@ -244,52 +267,31 @@ export default async function Department({ ))}
- + { + label: text.laboratories, + href: `/${locale}/academics/departments/laboratories`, + icon: HiMiniBeaker, + }, + { + label: text.achievements, + href: `/${locale}/academics/departments/${name}/achievements`, + icon: FaTrophy, + }, + ]} + /> + {imageCount !== 0 && ( - + ]} + /> ); } - -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}

      - -
    • - ))} -
    -
    -
  • - ) - ); -}; diff --git a/app/[locale]/academics/programmes/page.tsx b/app/[locale]/academics/programmes/page.tsx index 2e6afd5af..9f294d5e4 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 ( <> @@ -147,26 +173,15 @@ export default async function Programmes({ heading="h2" text={text.btech.toUpperCase()} /> -

    +

    {text.courseOfStudy} {text.btechAbout}


    - - - - {text.discipline} - {text.noOfSeats} - - - - {btech.map((programme, i) => ( - - {programme.name} - {programme.seats} - - ))} - -
    +
    @@ -175,35 +190,18 @@ export default async function Programmes({ heading="h2" text={text.mtech.toUpperCase()} /> -

    +

    {text.courseOfStudy} {text.mtechAbout}


    {text.secialization.toUpperCase()}

    - - - - {text.discipline} - {text.secialization} - - - - {mtech.map((programme, i) => ( - - {programme.name} - -
      - {programme.specializations.map((val, idx) => ( -
    • {val}
    • - ))} -
    -
    -
    - ))} -
    -
    +
    @@ -212,32 +210,11 @@ 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]/academics/scholarships/page.tsx b/app/[locale]/academics/scholarships/page.tsx index 67953afa2..7f9e99601 100644 --- a/app/[locale]/academics/scholarships/page.tsx +++ b/app/[locale]/academics/scholarships/page.tsx @@ -95,7 +95,7 @@ export default async function Scholarships({ description={text.HCS.about} portalHref="https://harchhatravratti.highereduhry.ac.in/" > -

    {text.HCS.objectives[0]}

    +

    {text.HCS.objectives[0]}

    -

    {text.RSSO.objectives[0]}

    +

    {text.RSSO.objectives[0]}

    -

    +

    {text.note.description}

    @@ -177,7 +177,9 @@ async function ScholarshipDisplay(props: ScholarshipProps) { >

    {text.about}

    -

    {props.about}

    +

    + {props.about} +

    {!props.description && props.portalHref && (

    {text.description}

    -

    {props.description}

    +

    {props.description}

    {props.portalHref && ( + {/* 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]/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]/events.tsx b/app/[locale]/events.tsx index 3debc3eff..45a8fe355 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 { AnimateEventsGrid } from './(animations)'; + +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, @@ -43,7 +53,7 @@ export default async function Events({ return (
    - {getKeys(text.categories).map((category, index) => ( + {DISPLAY_CATEGORIES.map((category, index) => (
  • ))} -
    +
    + + + + + {sortedDepartments.map(({ name, urlName }, index) => ( +
    + + + {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. + ))} +
    + ); +} diff --git a/app/[locale]/faculty-and-staff/faculty-image.tsx b/app/[locale]/faculty-and-staff/faculty-image.tsx new file mode 100644 index 000000000..57cdb7f14 --- /dev/null +++ b/app/[locale]/faculty-and-staff/faculty-image.tsx @@ -0,0 +1,85 @@ +'use client'; + +import { useState } from 'react'; +import Image from 'next/image'; + +import { env } from '~/lib/env/client'; +import { cn } from '~/lib/utils'; + +interface FacultyImageProps { + employeeId: string; + facultyId: number; + alt: string; + className?: string; + width?: number; + height?: number; + fill?: boolean; + sizes?: string; + /** The image URL from the persons.img field */ + imageUrl?: string | null; +} + +const PHOTO_EXTENSIONS = ['jpg', 'png', 'webp'] as const; +const FALLBACK_IMAGE = 'fallback/user-image.jpg'; + +export function FacultyImage({ + employeeId, + facultyId, + alt, + className, + width, + height, + fill, + sizes, + imageUrl, +}: FacultyImageProps) { + const [currentExtIndex, setCurrentExtIndex] = useState(0); + const [useFallback, setUseFallback] = useState(false); + + const handleError = () => { + // If we have an imageUrl from DB and it fails, go to fallback + if (imageUrl) { + setUseFallback(true); + return; + } + + if (currentExtIndex < PHOTO_EXTENSIONS.length - 1) { + // Try next extension + setCurrentExtIndex((prev) => prev + 1); + } else { + // All extensions failed, use fallback + setUseFallback(true); + } + }; + + // Prioritize imageUrl from DB, then try legacy path, then fallback + const imageSrc = useFallback + ? FALLBACK_IMAGE + : imageUrl + ? imageUrl + : `${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 740ed5e97..482be0a3b 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,13 @@ 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'; +import { FacultyImage } from './faculty-image'; export default async function FacultyAndStaff({ params: { locale }, @@ -38,6 +44,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 +77,7 @@ export default async function FacultyAndStaff({

    Department

    + }> @@ -76,9 +86,9 @@ export default async function FacultyAndStaff({
    - + - {/* Mobile Designation Filter */} - }> - - - - {/* Mobile Department Filter */} - }> - - +
    + +
      @@ -139,6 +146,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 +175,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]; - }; - - return select ? ( - - - {name} - -
      - ))} -
      - - ) : ( -
        - {departments.map(({ name, urlName }, index) => ( -
      1. - -
        -
        - -
        - {name} -
        -
        -
      2. - ))} -
      + return ( + ); }; @@ -344,13 +292,18 @@ const FacultyList = async ({ linkedInId: true, researchGateId: true, scopusId: true, + orcidId: true, id: true, }, where: selectedDepartmentIds.length ? (faculty, { inArray }) => inArray(faculty.departmentId, selectedDepartmentIds) : undefined, - with: { person: { columns: { email: true, name: true, telephone: true } } }, + with: { + person: { + columns: { email: true, name: true, telephone: true, img: true }, + }, + }, }); const filteredFaculty = faculty.filter(({ person }) => @@ -360,80 +313,131 @@ const FacultyList = async ({ return filteredFaculty.length === 0 ? ( ) : ( - filteredFaculty.map((faculty, index) => { - const isDepartmentHead = departmentHeads.find( - ({ facultyId }) => facultyId === faculty.id - ); + filteredFaculty + .sort((a, b) => { + 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, - }; + const profileExternalLinks = { + googleScholarId: faculty.googleScholarId, + linkedInId: faculty.linkedInId, + researchGateId: faculty.researchGateId, + scopusId: faculty.scopusId, + orcidId: faculty.orcidId, + }; - return ( -
    1. - - {faculty.person.name} -
      -
      -

      {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 && + profileExternalLinks[key] + ) { + return ( + + {key} + {text.externalLinks[key]} + + ); + } + })} +
      + {/* On small and medium screens */} + +
      {( Object.entries(profileExternalLinks) as [ keyof typeof profileExternalLinks, @@ -444,7 +448,7 @@ const FacultyList = async ({ return ( {text.externalLinks[key]} @@ -460,38 +464,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]} - - ); - } - })} -
      -
    2. - ); - }) + + ); + }) ); }; @@ -555,12 +530,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 f4f8509f0..573b82a0c 100644 --- a/app/[locale]/faculty-and-staff/utils.tsx +++ b/app/[locale]/faculty-and-staff/utils.tsx @@ -46,8 +46,10 @@ import { students, } from '~/server/db'; +import { FacultyImage } from './faculty-image'; + // Contains the content of full Faculty Profile -async function FacultyOrStaffComponent({ +export async function FacultyOrStaffComponent({ children, employeeId, id, @@ -112,6 +114,7 @@ async function FacultyOrStaffComponent({ linkedInId: true, researchGateId: true, scopusId: true, + orcidId: true, areasOfInterest: true, }, with: { @@ -123,6 +126,7 @@ async function FacultyOrStaffComponent({ countryCode: true, alternateTelephone: true, alternateCountryCode: true, + img: true, }, }, department: { @@ -144,6 +148,12 @@ 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'), }, }); @@ -189,7 +199,6 @@ async function FacultyOrStaffComponent({ ); const facultyDescription = { - doctoralStudents: 0, // Doctoral Student count not implemented ...facultyDescriptionTmp, // Original faculty details publications: realPublicationsCount, // Use the new count method }; @@ -237,12 +246,14 @@ async function FacultyOrStaffComponent({ )} - 0
      • @@ -280,19 +291,14 @@ async function FacultyOrStaffComponent({ {/* Faculty Image */}
        - 0
        {/* Faculty Intellectual Contribution counts */} @@ -317,19 +323,42 @@ async function FacultyOrStaffComponent({
    + {/* Faculty links to external profiles */} -
    - {( + {(() => { + // Pre-calculate which external links are present + const externalLinksEntries = ( Object.entries(text.externalLinks) as [ keyof typeof text.externalLinks, string, ][] - ).map(([key, value]) => { - if (key in facultyDescription) { - return ( + ).filter( + ([key]) => + key in facultyDescription && + facultyDescription[key as keyof typeof facultyDescription] + ); + const linkCount = externalLinksEntries.length; + + if (linkCount === 0) return null; + + return ( +
    + {externalLinksEntries.map(([key, value]) => ( {key} -
    {value}
    +
    + {value} +
    - ); - } - })} -
    + ))} +
    + ); + })()} {/* Faculty Intlectual Contribution counts */}
    @@ -433,7 +476,7 @@ const facultyTables = { outreachActivities: outreachActivities, } as const; -async function FacultySectionComponent({ +export async function FacultySectionComponent({ locale, facultySection, id, @@ -811,5 +854,3 @@ async function fetchSectionByFacultyId( } )?.[section]; } - -export { FacultyOrStaffComponent, FacultySectionComponent }; diff --git a/app/[locale]/footer.tsx b/app/[locale]/footer.tsx index 31a6bb69d..bc8920aab 100644 --- a/app/[locale]/footer.tsx +++ b/app/[locale]/footer.tsx @@ -12,6 +12,13 @@ import { MdMail, MdPhone } from 'react-icons/md'; import { getTranslations } from '~/i18n/translations'; import { cn } from '~/lib/utils'; +import { + AnimateFooterLinkColumn, + AnimateFooterLinksGrid, + AnimateSocialIcon, + AnimateSocialIcons, +} from './(animations)'; + export default async function Footer({ locale }: { locale: string }) { const text = (await getTranslations(locale)).Footer; @@ -26,6 +33,12 @@ export default async function Footer({ locale }: { locale: string }) { href: '/institute/sections/library/collection-and-resources', }, { name: 'Medical Facilities', href: '/institute/health-centre' }, + // Added: Register as Alumni + { + name: 'Register as Alumni', + href: 'https://forms.gle/yznDpHT2nYgHYCY97', + target: '_blank', + }, ]; const academicLinks = [ @@ -42,6 +55,18 @@ export default async function Footer({ locale }: { locale: string }) { href: '/academics/departments/computer-engineering/labs', }, { name: 'Research Publications', href: '/research/publications' }, + // Added: NAD Digilocker + { + name: 'NAD Digilocker', + href: 'https://nad.digilocker.gov.in/', + target: '_blank', + }, + // Added: NIT KKR @NDL + { + name: 'NIT KKR @NDL', + href: 'https://ndl.iitkgp.ac.in/result?q=%22t%22:%22sourceOrganization%22,%22k%22:%22NIT%20Kurukshetra%22,%22s%22:%5B%5D,%22b%22:%22browse%22:%22sourceOrganization%22,%22filters%22:%5B%22sourceOrganization=%22NIT%20Kurukshetra%22%22%5D', + target: '_blank', + }, ]; const resourceLinks = [ @@ -57,6 +82,16 @@ 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', + }, + // Added: Council of NITs + { + name: 'Council of NITs', + href: 'http://nitcouncil.org.in/', + target: '_blank', + }, ]; return ( @@ -102,55 +137,28 @@ export default async function Footer({ locale }: { locale: string }) { src="assets/pillar.png" /> -
    - + + - + - -
    + +
    @@ -160,42 +168,42 @@ export default async function Footer({ locale }: { locale: string }) {
  • {text.copyright}
  • -
      -
    1. + + -
    2. -
    3. + + -
    4. -
    5. + + -
    6. -
    7. + + -
    8. -
    9. + + -
    10. -
    + + diff --git a/app/[locale]/header.tsx b/app/[locale]/header.tsx index f84594f64..250c78de6 100644 --- a/app/[locale]/header.tsx +++ b/app/[locale]/header.tsx @@ -27,6 +27,8 @@ import { cn } from '~/lib/utils'; import { getServerAuthSession } from '~/server/auth'; import { db } from '~/server/db'; +import { AnimateHeader } from './(animations)'; + export default async function Header({ locale }: { locale: string }) { const text = (await getTranslations(locale)).Header; @@ -96,6 +98,13 @@ export default async function Header({ locale }: { locale: string }) { href: '/academics/convocation', description: 'Get information on upcoming convocation ceremonies.', }, + { + title: 'Admission', + href: '/academics/admission', + description: + 'Learn about admission process, eligibility, and application details.', + }, + // May remove 'Awards' from the Navigation { title: 'Awards', href: '/academics/awards', @@ -112,7 +121,7 @@ export default async function Header({ locale }: { locale: string }) { title: 'Academic Notifications', href: '/academics/notifications', description: - 'Stay updated with the latest academic announcements and deadlines.', + 'Stay updated with the latest academic notifications and announcements.', }, ], }, @@ -122,7 +131,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, }, { @@ -172,7 +181,7 @@ export default async function Header({ locale }: { locale: string }) { ]; return ( -
    + -
    + ); } @@ -390,23 +399,14 @@ const AuthAction = async ({ const session = await getServerAuthSession(); if (session) { - let id = ''; - if (session.person.type === 'faculty') { - id = (await db.query.faculty.findFirst({ - columns: { employeeId: true }, - where: (faculty, { eq }) => eq(faculty.id, session.person.id), - }))!.employeeId; - } else if (session.person.type === 'staff') { - id = (await db.query.staff.findFirst({ - columns: { employeeId: true }, - where: (staff, { eq }) => eq(staff.id, session.person.id), - }))!.employeeId; - } else if (session.person.type === 'student') { - id = (await db.query.students.findFirst({ - columns: { rollNumber: true }, - where: (student, { eq }) => eq(student.id, session.person.id), - }))!.rollNumber; - } + // Fetch the person's image from the database + const person = await db.query.persons.findFirst({ + columns: { img: true }, + where: (persons, { eq }) => eq(persons.id, session.person.id), + }); + + const profileImage = + person?.img ?? session.user.image ?? 'fallback/user-image.jpg'; return mobile ? ( + + ))} + + + + ); +} 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..621d09c1e 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..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 @@ -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)/committee.tsx b/app/[locale]/institute/administration/(committees)/committee.tsx index 62f846a48..1ad3f3518 100644 --- a/app/[locale]/institute/administration/(committees)/committee.tsx +++ b/app/[locale]/institute/administration/(committees)/committee.tsx @@ -83,10 +83,14 @@ export default async function Committee({ ); diff --git a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx index a55bddbe4..f39c50395 100644 --- a/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx +++ b/app/[locale]/institute/administration/(committees)/financial-committee/page.tsx @@ -1,16 +1,123 @@ -// 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, + 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, + })); + 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..121615ad4 --- /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..a03dfc598 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/deans/[name]/page.tsx b/app/[locale]/institute/administration/deans/[name]/page.tsx index ee9077b8b..3a1b5e295 100644 --- a/app/[locale]/institute/administration/deans/[name]/page.tsx +++ b/app/[locale]/institute/administration/deans/[name]/page.tsx @@ -1,21 +1,33 @@ import Image from 'next/image'; +import Link from 'next/link'; +import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; +import { Suspense } from 'react'; import { notFound } from 'next/navigation'; -import { MdEmail, MdLocationOn, MdPhone } from 'react-icons/md'; import Heading from '~/components/heading'; import { getTranslations } from '~/i18n/translations'; -import { cn } from '~/lib/utils'; +import ImageHeader from '~/components/image-header'; +import Loading from '~/components/loading'; +import GenericTable from '~/components/ui/generic-table'; import { db, deans } from '~/server/db'; + +import DeanCard from '../deans-card'; + export async function generateStaticParams() { return await db.select({ name: deans.domain }).from(deans); } -export default async function Dean({ +export default async function DeanCorner({ params: { locale, name: deanTitle }, }: { - params: { locale: string; name: (typeof deans.domain.enumValues)[number] }; + params: { + locale: string; + name: (typeof deans.domain.enumValues)[number]; + }; }) { - const text = (await getTranslations(locale)).Dean; + const text = (await getTranslations(locale)).DeansPage; + + // Fetch dean data from database const dean = await db.query.deans.findFirst({ where: (deans, { eq }) => eq(deans.domain, deanTitle), with: { @@ -31,6 +43,7 @@ export default async function Dean({ id: true, name: true, telephone: true, + img: true, }, }, }, @@ -38,71 +51,266 @@ export default async function Dean({ }, columns: { activityLogs: true, + message: true, + associateFacultyIds: true, + staffIds: true, + facultyInchargeIds: true, + contactNo: true, + email: true, }, }); + if (!dean) notFound(); + // Fetch associate deans data + const associateDeans = await db.query.faculty.findMany({ + where: (faculty, { inArray }) => + inArray(faculty.id, dean.associateFacultyIds), + columns: { + designation: true, + employeeId: true, + }, + with: { + person: { + columns: { + name: true, + email: true, + telephone: true, + img: true, + }, + }, + }, + }); + + // Fetch faculty incharges data + const facultyIncharges = await db.query.faculty.findMany({ + where: (faculty, { inArray }) => + inArray(faculty.id, dean.facultyInchargeIds), + columns: { + designation: true, + employeeId: true, + }, + with: { + person: { + columns: { + name: true, + email: true, + telephone: true, + img: true, + }, + }, + }, + }); + + // Fetch staff data + const staffData = await db.query.staff.findMany({ + where: (staffTable, { inArray }) => inArray(staffTable.id, dean.staffIds), + columns: { + designation: true, + personalEmail: true, + }, + with: { + person: { + columns: { + name: true, + telephone: true, + }, + }, + }, + }); + return ( <> - -
    -
    - {dean.faculty.person.name} + {/* ---------- DEAN'S PROFILE ---------- */} +
    + + +
    + + {/* ---------- DEAN'S MESSAGE ---------- */} +
    + +

    {dean.message}

    +
    + + {/* ---------- ASSOCIATE DEANS ---------- */} +
    + +
      + {associateDeans.map((ad, index) => ( +
    • + {ad.person.name} +
      +
      +

      + {ad.person.name} +

      + + {ad.designation} + +
      +
      + + + + {ad.person.email} + + + + + + {ad.person.telephone} + + +
      +
      +
    • + ))} +
    +
    + + {/* ---------- FACULTY INCHARGES ---------- */} +
    + +
      + {facultyIncharges.map((fi, index) => ( +
    • + {fi.person.name} +
      +
      +

      + {fi.person.name} +

      + + {fi.designation} + +
      +
      + + + + {fi.person.email} + + + + + + {fi.person.telephone} + + +
      +
      +
    • + ))} +
    +
    + + {/* ---------- RESPONSIBILITIES ---------- */} +
    + +
      + {dean.activityLogs.map((responsibility, index) => ( +
    • + {responsibility} +
    • + ))} +
    +
    - -
    - -
    - + + }> + ({ + name: s.person.name, + designation: s.designation, + contactNo: s.person.telephone || '-', + email: s.personalEmail ?? '-', + }))} + pageParamName="copyrightsPage" + getCount={Promise.resolve([])} /> -
      - {dean.activityLogs.map((responsibility, index) => ( -
    • -

      {responsibility}

      -
    • - ))} -
    -
    +
    ); diff --git a/app/[locale]/institute/administration/deans/deans-card.tsx b/app/[locale]/institute/administration/deans/deans-card.tsx new file mode 100644 index 000000000..f2020ba64 --- /dev/null +++ b/app/[locale]/institute/administration/deans/deans-card.tsx @@ -0,0 +1,83 @@ +import Image from 'next/image'; + +import { cn } from '~/lib/utils'; + +export default function DeanCard({ + 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} +
    • + {fax && ( +
    • + {labels.faxNo}{' '} + {fax} +
    • + )} +
    • + {labels.mobileNo}{' '} + {mobile} +
    • +
    • + {labels.emailId}{' '} + + {email} + +
    • +
    +
    +
    + ); +} diff --git a/app/[locale]/institute/administration/deans/page.tsx b/app/[locale]/institute/administration/deans/page.tsx index a2e48cae5..3e151ea5e 100644 --- a/app/[locale]/institute/administration/deans/page.tsx +++ b/app/[locale]/institute/administration/deans/page.tsx @@ -67,7 +67,7 @@ export default async function Deans({ 'bg-neutral-50 font-serif text-primary-700', 'rounded p-2 drop-shadow hover:drop-shadow-lg xl:p-3' )} - href={`/${locale}/institute/sections/${dean.domain}`} + href={`/${locale}/institute/administration/deans/${dean.domain}`} > + {/* 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..fad35635d 100644 --- a/app/[locale]/institute/administration/director/page.tsx +++ b/app/[locale]/institute/administration/director/page.tsx @@ -1,9 +1,161 @@ -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]/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 a87c5743c..1834ae99c 100644 --- a/app/[locale]/institute/administration/page.tsx +++ b/app/[locale]/institute/administration/page.tsx @@ -1,31 +1,19 @@ import Link from 'next/link'; import { Suspense } from 'react'; -import Image from 'next/image'; import { MdOutlineChecklist } from 'react-icons/md'; import { TbBuildings, TbContract, TbNotebook } from 'react-icons/tb'; import { LuShipWheel } from 'react-icons/lu'; import { VscMortarBoard } from 'react-icons/vsc'; import { HiCurrencyRupee } from 'react-icons/hi'; import { BsTools } from 'react-icons/bs'; -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 Loading from '~/components/loading'; export default async function Administration({ params: { locale }, @@ -61,22 +49,19 @@ export default async function Administration({ { label: text.administrationHeads, href: '#administration-heads' }, { label: text.committees, href: '#committees' }, { label: text.actsAndStatutes, href: '#acts-and-statutes' }, - { label: text.deans, href: '#deans' }, ]} /> -
    -

    - {text.description} -

    +
    - }> - - {text.composition} - - - - - {text.sNo} - {text.name} - {text.servedAs} - - - - - -
    -
    + }>
    + - - -
    - }> - - -
    ); } - -const SenateMembers = async () => { - 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 = [ - { - id: 1, - name: 'Professor Pratibha Aggarwal', - designation: 'Dean of Student Welfare', - email: 'jitenderchhabra@nitkkr.ac.in', - image: '/images/dean1.jpg', // replace with your actual dean image path - description: `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.`, - }, - { - id: 2, - name: 'Professor Pratibha Aggarwal', - designation: 'Dean of Student Welfare', - email: 'jitenderchhabra@nitkkr.ac.in', - image: '/images/dean1.jpg', // replace with your actual dean image path - description: `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.`, - }, - ]; - - return ( -
    - {deans.map((dean) => ( -
    - {/* Image + Email (left column) */} -
    - {dean.name} - -
    - - {/* Content (right column) */} -
    -
    -

    - {dean.name} -

    -

    - {dean.designation} -

    -
    - -

    - {dean.description} -

    -
    -
    - ))} -
    - ); -}; diff --git a/app/[locale]/institute/campus-infra/page.tsx b/app/[locale]/institute/campus-infra/page.tsx index 6b0552442..bf0ce8719 100644 --- a/app/[locale]/institute/campus-infra/page.tsx +++ b/app/[locale]/institute/campus-infra/page.tsx @@ -62,7 +62,7 @@ export default async function CampusInfra({
    -

    +

    {text.campus[1]} {text.campus[2]} {text.campus[3]} @@ -82,7 +82,7 @@ export default async function CampusInfra({ text={text.headings[1].toUpperCase()} />

    -

    +

    {text.infra[1]} {text.infra[2]}

    @@ -97,7 +97,7 @@ export default async function CampusInfra({ layout="intrinsic" alt="Image 1" /> -

    +

    {text.library.text[0]}

    @@ -113,7 +113,7 @@ export default async function CampusInfra({ layout="intrinsic" alt="Image 2" /> -

    +

    {text.computing.text[0]}

    @@ -128,7 +128,7 @@ export default async function CampusInfra({ layout="intrinsic" alt="Image 3" /> -

    +

    {text.senate.text[0]}

    @@ -143,7 +143,7 @@ export default async function CampusInfra({ layout="intrinsic" alt="Image 4" /> -

    +

    {text.sports.text[0]}

    @@ -159,7 +159,7 @@ export default async function CampusInfra({ text={text.headings[2].toUpperCase()} />
    -

    +

    {text.address[0]} {text.address[1]} {text.address[2]} diff --git a/app/[locale]/institute/cells/iic/page.tsx b/app/[locale]/institute/cells/iic/page.tsx index bd12a2031..d548f2584 100644 --- a/app/[locale]/institute/cells/iic/page.tsx +++ b/app/[locale]/institute/cells/iic/page.tsx @@ -1,12 +1,9 @@ +import Image from 'next/image'; + +import Gallery from '~/components/ui/gallery'; import Heading from '~/components/heading'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '~/components/ui'; +import GenericTable from '~/components/ui/generic-table'; +import FICGroup from '~/components/fic-group'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; @@ -19,83 +16,91 @@ export default async function IICPage({ const officeOrder = [ { - sr_no: 1, - responsibility: 'Dr. A. K. Mehta', - name_of_faculty: 'Professor', + responsibility: 'President IIC', + name_of_faculty: 'Prof. R. K. Sharma', + }, + + { + responsibility: 'Vice-President IIC', + name_of_faculty: 'Prof. Dinesh Khanduja', + }, + { + responsibility: 'Convener IIC', + name_of_faculty: 'Dr. Rajeev Rathi', }, { - sr_no: 2, - responsibility: 'Dr. A. K. Mehta', - name_of_faculty: 'Assistant Professor', + responsibility: 'Innovation Cell Coordinator', + name_of_faculty: 'Dr. Gulshan Sachdeva', }, { - sr_no: 3, - responsibility: 'Rohan Sharma', - name_of_faculty: 'Student', + responsibility: 'Start-up Cell Coordinator', + name_of_faculty: 'Dr. Pardeep Kumar', }, { - sr_no: 4, - responsibility: 'Dr. Sneha Verma', - name_of_faculty: 'Assistant Professor', + responsibility: 'Social Media Coordinator', + name_of_faculty: 'Dr. Shelly Vadhera', }, { - sr_no: 5, - responsibility: 'Dr. Sneha Verma', - name_of_faculty: 'Assistant Professor', + responsibility: 'Internship Coordinator', + name_of_faculty: 'Dr. M. P. R. Prashad', }, { - sr_no: 6, - responsibility: 'Dr. A. K. Mehta', - name_of_faculty: 'Professor', + responsibility: 'IPR Coordinator', + name_of_faculty: 'Prof. Lalit Mohan Saini', }, { - sr_no: 7, - responsibility: 'Ananya Gupta', - name_of_faculty: 'Student', + responsibility: 'ARIIA Coordinator', + name_of_faculty: 'Dr. Neeraj Kaushik', }, { - sr_no: 8, - responsibility: 'Dr. Ritesh Singh', - name_of_faculty: 'Associate Professor', + responsibility: 'NIRF Coordinator', + name_of_faculty: 'Dr. Sandeep Singhal', }, { - sr_no: 9, - responsibility: 'Dr. Neha Sharma', - name_of_faculty: 'Assistant Professor', + responsibility: 'Member IIC', + name_of_faculty: 'Dr. Saurabh Chanana', }, { - sr_no: 10, - responsibility: 'Dr. Neha Sharma', - name_of_faculty: 'Assistant Professor', + responsibility: 'Member IIC', + name_of_faculty: 'Dr. Gaurav Saini', + }, + { + responsibility: 'Member IIC', + name_of_faculty: 'Dr. Trailokya Nath Sasamal', + }, + { + responsibility: 'Member IIC', + name_of_faculty: 'Dr. Munish Bhatia', }, ]; + const activities = [ { - sr_no: 1, past_activity: "Workshop on 'Design Thinking for Innovation'", }, { - sr_no: 2, past_activity: "Talk on 'From Campus to Startup' by Alumni Entrepreneurs", }, { - sr_no: 3, past_activity: 'Idea Pitching Competition with Industry Mentors', }, { - sr_no: 4, past_activity: "Seminar on 'AI and the Future of Startups'", }, { - sr_no: 5, past_activity: 'Innovation Bootcamp: 3-Day Product Building Sprint', }, { - sr_no: 6, + past_activity: 'Celebration of National Technology Day with Tech Demos', + }, + { past_activity: 'Celebration of National Technology Day with Tech Demos', }, ]; + const galleryImages = Array.from({ length: 23 }, (_, i) => ({ + src: `institute/cells/iks/${i + 1}.jpg`, + })); return ( <> {/* Header */} @@ -119,104 +124,154 @@ export default async function IICPage({

    {/* Description */} -
    -

    - - {text.Institute.cells.iic.preamble}:{' '} - +

    +

    {text.Institute.cells.iic.description}

    +
    + {/* Top/Left: Vision & Mission Text */} +
    +
    +

    + {text.Institute.cells.iic.vision.title} +

    + {text.Institute.cells.iic.vision.content.map((vision, index) => ( +

    + {vision} +

    + ))} +
    + +
    +

    + {text.Institute.cells.iic.mission.title} +

    + {text.Institute.cells.iic.mission.content.map( + (mission, index) => ( +

    + {mission} +

    + ) + )} +
    +
    + + {/* Right/Bottom: Full coverage image */} +
    + Institute IIC pillars illustration +
    +
    + {/* Office Order */}
    - - - - - {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 */}
    {/* 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} - - ))} - -
    +
    + + {text.Institute.cells.iic.employes && + (() => { + const iicEmployeeIds = ['87', '130', '1578']; + const iicDesignations = text.Institute.cells.iic.employes.map( + (e) => e.position + ); + const facultyData = iicEmployeeIds.map((id, idx) => ({ + employeeId: id, + designation: iicDesignations[idx] ?? '', + })); + + return ; + })()} + + +
    ); diff --git a/app/[locale]/institute/cells/iks/page.tsx b/app/[locale]/institute/cells/iks/page.tsx index d09a5d6fc..b1dfebdc0 100644 --- a/app/[locale]/institute/cells/iks/page.tsx +++ b/app/[locale]/institute/cells/iks/page.tsx @@ -1,6 +1,33 @@ -import { getS3Url } from '~/server/s3'; -import { getTranslations } from '~/i18n/translations'; +import { like, or } from 'drizzle-orm'; + +import FICGroup from '~/components/fic-group'; import Gallery from '~/components/ui/gallery'; +import { getTranslations } from '~/i18n/translations'; +import { db } from '~/server/db'; +import { otherOfficers } from '~/server/db/schema'; +import { getS3Url } from '~/server/s3'; + +// Function to fetch IKS faculty-in-charge from otherOfficers table +async function fetchIKSFaculty() { + const iksOfficers = await db.query.otherOfficers.findMany({ + where: or( + like(otherOfficers.designation, '%IKS%'), + like(otherOfficers.designation, '%Indian Knowledge System%') + ), + with: { + faculty: { + columns: { + employeeId: true, + }, + }, + }, + }); + + return iksOfficers.map((officer) => ({ + employeeId: officer.faculty.employeeId, + designation: officer.designation, + })); +} export default async function IKS({ params: { locale }, @@ -9,39 +36,75 @@ 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.', }, - ]; - const members = [ { - name: 'Prof RK Aggarwal', - designation: 'Prof-In-Charge, IKS Cell', + id: 4, + description: + 'IKS Cell celebrated the Pran Pratishtha Ceremony of Lord Rama at Ayodhya on 22.01.2024 at Bhagirathi Bhawan, 11 AM.', }, { - name: 'Dr. Shabnam', - designation: 'Faculty-In-Charge, IKS Cell', + id: 5, + description: + 'IKS Cell performed Hawan on 11.10.2024 at Kalpna Chawla Bhawan, Girls Hostel, 11 AM.', }, { - name: 'Dr. Kuldeep Kumar', - designation: 'Faculty-In-Charge, IKS Cell', + 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 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`, + })); + + // Fetch faculty-in-charge data from database + const facultyData = await fetchIKSFaculty(); + return ( <> {/* heading */} @@ -67,40 +130,70 @@ export default async function IKS({ {/* description */}

    {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}

    -
      - {members.map((member) => ( -
    • - {member.name}, {member.designation} + +

      + {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} +

    +
      + {book.map((book) => ( +
    • + {book.author}, {book.title} +
    • + ))} +
    +
    + {/* Gallery */}
    -

    Gallery

    - +

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

    +
    diff --git a/app/[locale]/institute/cells/ipr/page.tsx b/app/[locale]/institute/cells/ipr/page.tsx index 052e36072..2e0f15053 100644 --- a/app/[locale]/institute/cells/ipr/page.tsx +++ b/app/[locale]/institute/cells/ipr/page.tsx @@ -1,24 +1,16 @@ import Image from 'next/image'; import Link from 'next/link'; -import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; import { FaFlask, FaIndianRupeeSign } from 'react-icons/fa6'; import { FaRegIdCard } from 'react-icons/fa'; import { BsTools } from 'react-icons/bs'; 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'; +import FICGroup from '~/components/fic-group'; +import ButtonGroup from '~/components/button-group'; // Fetches IPR data from DB - cache for 5 minutes export const revalidate = 300; @@ -50,91 +42,74 @@ export default async function IPR({ ]; const facultyIncharge = [ + // Replace with real employee IDs and designations as needed { - image: 'fallback/user-image.jpg', - name: 'Anshu Parashar', - title: 'Computer Application', - email: 'anshuparashar@nitkkr.ac.in', - phone: '1234567890', + employeeId: '88', + designation: 'Faculty Incharge', }, { - image: 'fallback/user-image.jpg', - name: 'Anshu Parashar', - title: 'Computer Application', - email: 'anshuparashar@nitkkr.ac.in', - phone: '1234567890', + employeeId: '89', + designation: 'Faculty Incharge', }, ]; 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', @@ -178,7 +153,7 @@ export default async function IPR({
    {/* description */}
    -

    +

    {text.Research.ipr.description}

    @@ -189,49 +164,7 @@ export default async function IPR({ heading="h2" text={text.Research.ipr.facultyIncharge} /> -
      - {facultyIncharge.map((faculty, idx) => ( -
    • - {faculty.name} -
      -
      -

      - {faculty.name} - - {faculty.title} - -

      -
      -
      - - - - {faculty.email} - - - - - - {faculty.phone} - - -
      -
      -
    • - ))} -
    + {/* Advisory Commitee */}
    @@ -242,34 +175,23 @@ 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 */} @@ -323,37 +245,7 @@ export default async function IPR({ {text.Research.ipr.availableTechnologies.description} -
    - {availableTechnologies.map(({ label, href, icon: Icon }, index) => ( - - ))} -
    + {/* NITKKR innovations and IP */}
    @@ -362,32 +254,7 @@ export default async function IPR({ heading="h2" text={text.Research.ipr.nitkkrInnovationsAndIp.title} /> -
    - {innovations.map(({ label, href, icon: Icon }, index) => ( - - ))} -
    +
    {/* Gallery */}
    diff --git a/app/[locale]/institute/cells/obcpwd/page.tsx b/app/[locale]/institute/cells/obcpwd/page.tsx index 036f0eda5..d0310d38c 100644 --- a/app/[locale]/institute/cells/obcpwd/page.tsx +++ b/app/[locale]/institute/cells/obcpwd/page.tsx @@ -1,12 +1,30 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; +import { like, or } from 'drizzle-orm'; -import { cn } from '~/lib/utils'; -import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; +import FICGroup from '~/components/fic-group'; +import { db } from '~/server/db'; +import { otherOfficers } from '~/server/db/schema'; + +// Function to fetch OBC/PWD liaison officer from otherOfficers table +async function fetchOBCPWDFaculty() { + const obcpwdOfficers = await db.query.otherOfficers.findMany({ + where: or(like(otherOfficers.designation, '%obc%')), + with: { + faculty: { + columns: { + employeeId: true, + }, + }, + }, + }); + + return obcpwdOfficers.map((officer) => ({ + employeeId: officer.faculty.employeeId, + designation: officer.designation, + })); +} export default async function OBCPWD({ params: { locale }, @@ -15,11 +33,11 @@ export default async function OBCPWD({ }) { const text = await getTranslations(locale); - const facultyIncharge = [ - {...text.Institute.cells.obcpwd.liaisonOfficer}, - ]; const cellFunctions = text.Institute.cells.obcpwd.cellFunctions; + // Fetch faculty-in-charge data from database + const facultyData = await fetchOBCPWDFaculty(); + return ( <> {/* Header */} @@ -29,8 +47,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 +56,7 @@ export default async function OBCPWD({
    {/* description */} -
    +
    {text.Institute.cells.obcpwd.description.map((paragraph, index) => (

    {paragraph} @@ -54,18 +72,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 */}
    @@ -75,75 +93,10 @@ export default async function OBCPWD({ text={text.Institute.cells.obcpwd.liaisonOfficerHeading} className="mt-12" /> -
      - {facultyIncharge.map((faculty, idx) => ( -
    • - {/* 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 */} - {faculty.title} -

      - {!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} - - -
      -
      -
    • - ))} -
    +
    - + {/* 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..441114c0b 100644 --- a/app/[locale]/institute/cells/scst/page.tsx +++ b/app/[locale]/institute/cells/scst/page.tsx @@ -1,12 +1,32 @@ -import Image from 'next/image'; -import Link from 'next/link'; -import { MdEmail, MdOutlineLocalPhone } from 'react-icons/md'; +import { like, or } from 'drizzle-orm'; import { cn } from '~/lib/utils'; import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; import { getTranslations } from '~/i18n/translations'; import { getS3Url } from '~/server/s3'; +import FICGroup from '~/components/fic-group'; +import { db } from '~/server/db'; +import { otherOfficers } from '~/server/db/schema'; + +// Function to fetch SC/ST liaison officer from otherOfficers table +async function fetchSCSTFaculty() { + const scstOfficers = await db.query.otherOfficers.findMany({ + where: or(like(otherOfficers.designation, '%sc-st%')), + with: { + faculty: { + columns: { + employeeId: true, + }, + }, + }, + }); + + return scstOfficers.map((officer) => ({ + employeeId: officer.faculty.employeeId, + designation: officer.designation, + })); +} export default async function SCST({ params: { locale }, @@ -15,12 +35,12 @@ export default async function SCST({ }) { const text = await getTranslations(locale); - const facultyIncharge = [ - {...text.Institute.cells.scst.liaisonOfficer}, - ]; const cellFunctions = text.Institute.cells.scst.cellFunctions; const importantLinks = text.Institute.cells.scst.importantLinks; + // Fetch faculty-in-charge data from database + const facultyData = await fetchSCSTFaculty(); + return ( <> {/* Header */} @@ -30,8 +50,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 +59,7 @@ export default async function SCST({
    {/* description */} -
    +
    {text.Institute.cells.scst.description.map((paragraph, index) => (

    {paragraph} @@ -55,18 +75,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 */}
    @@ -76,71 +96,7 @@ export default async function SCST({ text={text.Institute.cells.scst.liaisonOfficerHeading} className="mt-12" /> -
      - {facultyIncharge.map((faculty, idx) => ( -
    • - {/* 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 */} - {faculty.title} -

      - {!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} - - -
      -
      -
    • - ))} -
    +
    {/* IMPORTANT LINKS */} {/* TODO: MAKE IT EXACTLY LIKE THE DESIGN , ADD RELEVENT BACKGROUND */} @@ -151,12 +107,12 @@ export default async function SCST({ text={text.Institute.cells.scst.importantLinksHeading} className="mt-12" /> - + - - ); diff --git a/app/[locale]/institute/hostels/[url_name]/page.tsx b/app/[locale]/institute/hostels/[url_name]/page.tsx index 2284813df..5e650d9d7 100644 --- a/app/[locale]/institute/hostels/[url_name]/page.tsx +++ b/app/[locale]/institute/hostels/[url_name]/page.tsx @@ -4,16 +4,9 @@ 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'; +import { db } from '~/server/db'; // Fetches hostel data from DB - cache for 1 hour export const revalidate = 3600; @@ -146,32 +139,40 @@ 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, + }; + })} + pageParamName={index === 0 ? 'faculty-page' : 'staff-page'} + /> ))}
    diff --git a/app/[locale]/institute/hostels/page.tsx b/app/[locale]/institute/hostels/page.tsx index 89ab2f2c7..2cd020e35 100644 --- a/app/[locale]/institute/hostels/page.tsx +++ b/app/[locale]/institute/hostels/page.tsx @@ -3,18 +3,17 @@ import Link from 'next/link'; import { Suspense } from 'react'; import { MdBadge } from 'react-icons/md'; -import { Button } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; 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/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'}> - - -
    -
    -
    +
    + +
    - +
    ); diff --git a/app/[locale]/institute/page.tsx b/app/[locale]/institute/page.tsx index 543077c02..1e7f686e6 100644 --- a/app/[locale]/institute/page.tsx +++ b/app/[locale]/institute/page.tsx @@ -1,24 +1,16 @@ import Image from 'next/image'; -import Link from 'next/link'; import { BsBuilding, BsDownload } from 'react-icons/bs'; import { FaUsers } from 'react-icons/fa'; import { FaBuildingColumns } from 'react-icons/fa6'; import { MdPhotoLibrary } from 'react-icons/md'; import { PiTreeStructureFill } from 'react-icons/pi'; -import { BouncyArrowButton, Button } from '~/components/buttons'; +import { BouncyArrowButton } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; 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'; export default async function Institute({ params: { locale }, @@ -209,50 +201,21 @@ 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, + }))} + /> {/* FUNDS */} @@ -293,13 +256,9 @@ export default async function Institute({ id="cells" text={text.cells.headingTitle.toUpperCase()} /> - + ]} + /> {/* QUICK LINKS */} @@ -361,13 +302,9 @@ export default async function Institute({ id="quick-links" text={text.quickLinks.title.toUpperCase()} /> - + ]} + /> ); diff --git a/app/[locale]/institute/sections/accounts/page.tsx b/app/[locale]/institute/sections/accounts/page.tsx index 66c680754..c902d354b 100644 --- a/app/[locale]/institute/sections/accounts/page.tsx +++ b/app/[locale]/institute/sections/accounts/page.tsx @@ -73,7 +73,7 @@ export default async function Accounts({ heading="h3" href="#about" /> -

    {section?.aboutUs}

    +

    {section?.aboutUs}

    diff --git a/app/[locale]/institute/sections/central-workshop/page.tsx b/app/[locale]/institute/sections/central-workshop/page.tsx index 8b6948b6e..6c3210bb4 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,95 @@ 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, + }))} + pageParamName={`${category}-page`} + /> + + {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, + }))} + pageParamName="staff-page" + /> + ); }; diff --git a/app/[locale]/institute/sections/estate/page.tsx b/app/[locale]/institute/sections/estate/page.tsx index 5648eaef6..d4cdc738b 100644 --- a/app/[locale]/institute/sections/estate/page.tsx +++ b/app/[locale]/institute/sections/estate/page.tsx @@ -1,25 +1,16 @@ // Revalidate every hour (has DB calls, rarely changes) export const revalidate = 3600; -import Link from 'next/link'; import React from 'react'; import { Suspense } from 'react'; import { MdArticle } from 'react-icons/md'; -import { Button } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; 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'; export default async function Estate({ @@ -35,220 +26,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 +212,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 +403,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 +435,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 +467,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 +569,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', }, @@ -747,33 +658,13 @@ export default async function Estate({ {text.about[0]} {text.about[1]}

    - + ({ + label: text, + href, + icon, + }))} + />
    @@ -786,24 +677,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 +698,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 +805,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, + }))} + pageParamName="aadPage" + 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, + }))} + pageParamName="hadPage" + 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, + }))} + pageParamName="hdPage" + 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, + }))} + pageParamName="ghdPage" + 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, + }))} + pageParamName="radPage" + 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, + }))} + pageParamName="sfdPage" + getCount={Promise.resolve([])} + />
    @@ -1177,45 +954,42 @@ export default async function Estate({ />

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

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

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

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

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

    - - - {text.project.future.slice(0).map((project, index) => ( - - {project} - - ))} - -
    + ({ + project, + }))} + pageParamName="fpPage" + getCount={Promise.resolve([])} + />
    @@ -1227,19 +1001,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 3115d4459..3822bfb59 100644 --- a/app/[locale]/institute/sections/health-centre/page.tsx +++ b/app/[locale]/institute/sections/health-centre/page.tsx @@ -7,15 +7,7 @@ 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 { - 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({ @@ -25,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', @@ -487,29 +475,22 @@ 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(', '), + }))} + pageParamName="timings-page" + />
    @@ -522,49 +503,35 @@ 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, + }))} + pageParamName="officers-page" + />
    }>

    {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, + }))} + pageParamName="staff-page" + />
    @@ -718,27 +685,19 @@ 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, + }))} + 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 2fc072bdb..036170570 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,22 @@ 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, + }))} + />
    ); 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..39638fd5e 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,14 @@ 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} - - ))} - -
    +
    @@ -120,10 +102,14 @@ export default async function MembershipAndPrivileges({

    {text.privileges.careOfBooks}

    1. -

      {text.privileges.careofBooksDescriptionOne}

      +

      + {text.privileges.careofBooksDescriptionOne} +

    2. -

      {text.privileges.careofBooksDescriptionTwo}

      +

      + {text.privileges.careofBooksDescriptionTwo} +

    @@ -132,13 +118,13 @@ export default async function MembershipAndPrivileges({

    {text.privileges.otherFacilities}

    1. -

      +

      {text.privileges.reprographicFacilities} {text.privileges.reprographicFacilitiesDescription}

    2. -

      +

      {text.privileges.binding} {text.privileges.bindingDescription}

      diff --git a/app/[locale]/institute/sections/library/page.tsx b/app/[locale]/institute/sections/library/page.tsx index cb4bf243e..631ac02cd 100644 --- a/app/[locale]/institute/sections/library/page.tsx +++ b/app/[locale]/institute/sections/library/page.tsx @@ -5,17 +5,11 @@ import { BsBook, BsTag } from 'react-icons/bs'; import { FaUsers } from 'react-icons/fa'; import { Button } from '~/components/buttons'; +import ButtonGroup from '~/components/button-group'; 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'; @@ -114,7 +108,7 @@ export default async function Library({

      {text.heading.aboutText} @@ -207,23 +201,17 @@ export default async function Library({

    3. -
      +
      - + ]} + />
      @@ -270,27 +240,15 @@ 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/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 c6a27db66..b170d4d70 100644 --- a/app/[locale]/notifications.tsx +++ b/app/[locale]/notifications.tsx @@ -1,25 +1,55 @@ import Link from 'next/link'; -import { Suspense } from 'react'; -import { MdOutlineKeyboardArrowRight } from 'react-icons/md'; -import { Button } from '~/components/buttons'; import Heading from '~/components/heading'; -import Loading from '~/components/loading'; -import { ScrollArea } from '~/components/ui'; +import NotificationsPanel from '~/components/notifications/notifications-panel'; import { getTranslations } from '~/i18n/translations'; -import { cn, getKeys, groupBy } from '~/lib/utils'; -import type { notifications as notificationsSchema } from '~/server/db'; -import { db } from '~/server/db'; +import { cn } from '~/lib/utils'; import { getS3Url } from '~/server/s3'; +// Categories shown in the home page notification section +// 'tender' is handled specially - it redirects to the dedicated tenders page +const notificationCategories = [ + 'academic', + 'tender', // Special case: redirects to /notifications/tenders + 'workshop', + 'recruitment', +] as const; + +// Categories that use the NotificationsPanel (excludes 'tender') +const panelCategories = ['academic', 'workshop', 'recruitment'] as const; +type PanelCategory = (typeof panelCategories)[number]; + +export type NotificationCategory = (typeof notificationCategories)[number]; + export default async function Notifications({ category: currentCategory, locale, }: { - category: (typeof notificationsSchema.category.enumValues)[number]; + category: NotificationCategory; locale: string; }) { const text = (await getTranslations(locale)).Notifications; + const tendersText = (await getTranslations(locale)).Tenders; + + // Get the category label - 'tender' uses the Tenders translation + const getCategoryLabel = (category: NotificationCategory) => { + if (category === 'tender') { + return tendersText.title; + } + return text.categories[category]; + }; + + // Get the href for a category - 'tender' redirects to the dedicated page + const getCategoryHref = (category: NotificationCategory) => { + if (category === 'tender') { + return `/${locale}/notifications/tenders`; + } + return { query: { notificationCategory: category } }; + }; + + // Determine which category to show in the panel (fallback to 'academic' if 'tender' is selected) + const panelCategory: PanelCategory = + currentCategory === 'tender' ? 'academic' : currentCategory; return (
      @@ -44,12 +74,12 @@ export default async function Notifications({ 'lg:w-[30%] lg:flex-col lg:justify-between lg:bg-transparent lg:p-0' )} > - {getKeys(text.categories).map((category, index) => ( + {notificationCategories.map((category, index) => (
    4. ))}
    -
    - -
      - } 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}

      - -
    • - ))} -
    -
    -
  • - ) - ); -}; diff --git a/app/[locale]/notifications/NotificationForm.tsx b/app/[locale]/notifications/NotificationForm.tsx new file mode 100644 index 000000000..14213f3f7 --- /dev/null +++ b/app/[locale]/notifications/NotificationForm.tsx @@ -0,0 +1,382 @@ +'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 { type JSONContent } from '@tiptap/react'; + +import { Button } from '~/components/buttons'; +import { Input, Textarea } from '~/components/inputs'; +import { RichTextEditor } from '~/components/editor'; +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; + richContent: unknown; + 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 [richContent, setRichContent] = useState( + (initialData?.richContent as JSONContent) ?? null + ); + 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); + + // Deep-clone richContent to plain objects so it can be serialized + // for Server Actions (TipTap JSONContent may carry class prototypes) + const plainRichContent = richContent + ? (JSON.parse(JSON.stringify(richContent)) as typeof richContent) + : undefined; + + const data: NotificationFormData = { + title: title.trim(), + content: content.trim() || undefined, + richContent: plainRichContent, + 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" + /> +
    + + {/* Description (plain text) */} +
    + +