Wire updates, events, minutes, deaf-focus pages to backend API
Replace static arrays with Next.js Server Component fetch() calls. Add express.static for /uploads. Revalidate every hour.
This commit is contained in:
parent
0b979226f0
commit
5f94462afd
6 changed files with 250 additions and 378 deletions
|
|
@ -4,6 +4,7 @@ import helmet from 'helmet';
|
|||
import morgan from 'morgan';
|
||||
import mongoose from 'mongoose';
|
||||
import dotenv from 'dotenv';
|
||||
import path from 'path';
|
||||
import { rateLimit } from 'express-rate-limit';
|
||||
|
||||
// Load environment variables
|
||||
|
|
@ -47,6 +48,9 @@ const connectDB = async () => {
|
|||
}
|
||||
};
|
||||
|
||||
// Serve uploaded files (PDFs, images)
|
||||
app.use('/uploads', express.static(path.join(process.cwd(), 'uploads')));
|
||||
|
||||
// Middleware
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
|
|
|||
7
frontend/.env.example
Normal file
7
frontend/.env.example
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
# Server-to-server API URL (Next.js Server Components → Express backend)
|
||||
# Docker: http://backend:4000 | Local dev: http://localhost:4000
|
||||
BACKEND_URL=http://backend:4000
|
||||
|
||||
# Browser-accessible backend URL (used for document download links in rendered HTML)
|
||||
# Must be publicly reachable — set to your Caddy-fronted domain or IP
|
||||
NEXT_PUBLIC_BACKEND_URL=https://api.lad1908.org
|
||||
|
|
@ -8,67 +8,30 @@ export const metadata: Metadata = {
|
|||
description: 'Deaf Focus is a Deaf-led organization operated by LAD offering sign language interpreting, advocacy, ASL classes, and community resources across Louisiana.',
|
||||
};
|
||||
|
||||
const articles = [
|
||||
{
|
||||
title: 'What is Deaf Focus?',
|
||||
body: 'Deaf Focus is a Deaf-led organization operated by the Louisiana Association of the Deaf. We offer sign language interpreting as our primary service and serve Deaf, DeafBlind, and hard-of-hearing individuals throughout Louisiana. Our team emphasizes culturally appropriate, accessible communication using ASL as the primary mode. We recommend 30+ days advance notice for certified interpreter assignments.',
|
||||
contact: 'request@deaffocus.org | (225) 319-5586',
|
||||
},
|
||||
{
|
||||
title: 'How Do We Use the "D" in Deaf?',
|
||||
body: 'Lowercase "deaf" refers to the audiological or medical condition of hearing loss. Uppercase "Deaf" signals a cultural identity — someone who uses sign language as their primary language and identifies as a member of the Deaf community. Writing "d/Deaf" is considered the most inclusive form. At LAD and Deaf Focus, we view Deafness as membership in a linguistic and cultural minority, not as a disability.',
|
||||
contact: null,
|
||||
},
|
||||
{
|
||||
title: 'Our Services',
|
||||
body: null,
|
||||
services: [
|
||||
'Professional sign language interpreting — in-person and remote (VRI)',
|
||||
'ADA compliance advocacy, education and healthcare navigation, legislative initiatives',
|
||||
'Resource hub and referrals for Deaf, DeafBlind, and hard-of-hearing individuals',
|
||||
'Deaf professional networking and mentorship',
|
||||
'Case management (Deaf-majority team): intake, goal setting, employment readiness, independent living',
|
||||
'Training for law enforcement, first responders, healthcare providers, businesses, and government',
|
||||
'ASL classes — virtual via Zoom (ages 15+) and in-person at EBRPL branches',
|
||||
'ASL tutoring — hourly, personalized sessions',
|
||||
],
|
||||
contact: null,
|
||||
},
|
||||
{
|
||||
title: 'Community',
|
||||
body: 'LAD is Louisiana\'s only truly Deaf-led statewide organization. We host a Monthly ASL Meet & Greet at Torchy\'s Tacos every last Thursday of the month (except November and December), welcoming all skill levels. Confidence Building events are organized for specific demographics throughout the year. Follow LAD on Facebook for the latest community updates and event announcements.',
|
||||
contact: null,
|
||||
},
|
||||
{
|
||||
title: 'ASL Classes & Tutoring',
|
||||
body: 'Deaf Focus offers beginner-to-intermediate ASL classes several times per year:\n• Virtual classes via Zoom — ages 15 and up, occasional family and baby sign language sessions\n• In-person classes at East Baton Rouge Parish Library branches — waitlist available (contact mpugh@ebrpl.com)\n• Personalized ASL tutoring and mentoring — hourly sessions tailored to your level\n• Occasional family and baby sign language workshops\nCheck Eventbrite for the current class schedule and registration.',
|
||||
contact: 'info@deaffocus.org',
|
||||
},
|
||||
{
|
||||
title: 'Interpreting Services — Frequently Asked Questions',
|
||||
faq: [
|
||||
{
|
||||
q: 'Who is responsible for requesting and paying for an interpreter?',
|
||||
a: 'Under the ADA, covered entities (hospitals, courts, businesses, schools, government agencies) are responsible for requesting and paying for qualified interpreters when serving deaf individuals. Tax credit Form 8826 may apply for small businesses.',
|
||||
},
|
||||
{
|
||||
q: 'What credentials do your interpreters hold?',
|
||||
a: 'All certified interpreters hold RID (Registry of Interpreters for the Deaf) or BEI (Board for Evaluation of Interpreters) credentials.',
|
||||
},
|
||||
{
|
||||
q: 'How far in advance should I request an interpreter?',
|
||||
a: '10 business days notice is recommended for most assignments. Same-day requests are accepted but may not be guaranteed. For events lasting more than 1.25–1.5 hours, two interpreters are required (rotating every 15–20 minutes).',
|
||||
},
|
||||
{
|
||||
q: 'What are your hours and how do I pay?',
|
||||
a: 'Office hours: Monday–Friday, 9 AM–4 PM. Invoices are sent after service. We accept checks, money orders, and credit cards. 24/7 emergency services are available for life-threatening situations.',
|
||||
},
|
||||
],
|
||||
contact: 'request@deaffocus.org | (225) 319-5586 | Fax: (225) 308-4025',
|
||||
},
|
||||
];
|
||||
interface Article {
|
||||
_id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
async function getArticles(): Promise<Article[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.BACKEND_URL}/api/articles?status=published`,
|
||||
{ next: { revalidate: 3600 } }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.articles ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DeafFocusPage() {
|
||||
const articles = await getArticles();
|
||||
|
||||
export default function DeafFocusPage() {
|
||||
return (
|
||||
<div className="bg-color-bg">
|
||||
{/* Hero */}
|
||||
|
|
@ -90,16 +53,10 @@ export default function DeafFocusPage() {
|
|||
ASL classes, and community resources across Louisiana.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-wrap justify-center gap-4">
|
||||
<a
|
||||
href="tel:+12253195586"
|
||||
className="btn-primary inline-block"
|
||||
>
|
||||
<a href="tel:+12253195586" className="btn-primary inline-block">
|
||||
(225) 319-5586
|
||||
</a>
|
||||
<a
|
||||
href="mailto:request@deaffocus.org"
|
||||
className="btn-secondary inline-block"
|
||||
>
|
||||
<a href="mailto:request@deaffocus.org" className="btn-secondary inline-block">
|
||||
request@deaffocus.org
|
||||
</a>
|
||||
</div>
|
||||
|
|
@ -111,49 +68,24 @@ export default function DeafFocusPage() {
|
|||
<section className="py-16 bg-color-bg">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto space-y-10">
|
||||
{articles.map((article, i) => (
|
||||
<article key={i} className="card event-card-gold-border p-8">
|
||||
<h2 className="card-title event-card-title-gold text-2xl font-bold mb-4">
|
||||
{article.title}
|
||||
</h2>
|
||||
|
||||
{article.body && (
|
||||
<div className="section-text space-y-3">
|
||||
{article.body.split('\n').map((line, j) => (
|
||||
<p key={j}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{'services' in article && article.services && (
|
||||
<ul className="mt-4 space-y-2">
|
||||
{article.services.map((svc, j) => (
|
||||
<li key={j} className="flex items-start gap-2 section-text">
|
||||
<span className="text-[var(--color-primary)] font-bold mt-0.5">•</span>
|
||||
<span>{svc}</span>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{'faq' in article && article.faq && (
|
||||
<div className="mt-4 space-y-6">
|
||||
{article.faq.map((item, j) => (
|
||||
<div key={j}>
|
||||
<p className="font-semibold card-title mb-1">{item.q}</p>
|
||||
<p className="section-text">{item.a}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{article.contact && (
|
||||
<p className="mt-6 text-sm font-medium text-[var(--color-primary)]">
|
||||
Contact: {article.contact}
|
||||
</p>
|
||||
)}
|
||||
</article>
|
||||
))}
|
||||
{articles.length === 0 ? (
|
||||
<p className="section-text text-center py-12">Content coming soon.</p>
|
||||
) : (
|
||||
articles.map((article) => (
|
||||
<article key={article._id} className="card event-card-gold-border p-8">
|
||||
<h2 className="card-title event-card-title-gold text-2xl font-bold mb-4">
|
||||
{article.title}
|
||||
</h2>
|
||||
{article.body && (
|
||||
<div className="section-text space-y-3">
|
||||
{article.body.split('\n').map((line, i) => (
|
||||
<p key={i}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -8,130 +8,54 @@ export const metadata: Metadata = {
|
|||
};
|
||||
|
||||
interface Event {
|
||||
_id: string;
|
||||
title: string;
|
||||
date: string;
|
||||
time: string;
|
||||
location: string;
|
||||
description: string;
|
||||
slug: string;
|
||||
featured?: boolean;
|
||||
category: string;
|
||||
}
|
||||
|
||||
const events: Event[] = [
|
||||
{
|
||||
title: 'LAD 52nd Biennial State Conference',
|
||||
date: 'June 1–3, 2023',
|
||||
location: 'Sheraton Metairie, New Orleans area',
|
||||
description:
|
||||
'"Embracing the Challenges of Change" — Keynote by Dr. Natalie Delgado, 115th Anniversary Bash, comedian Sheena Lyles. $100 registration. Open to all.',
|
||||
slug: '52nd-biennial-conference',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
title: 'NOBDA & LAD Collaboration — Book Reading with Franklin Jones on BASL',
|
||||
date: 'Saturday, May 23rd',
|
||||
location: 'To be announced',
|
||||
description:
|
||||
'A special book reading event in collaboration with the New Orleans Black Deaf Association, featuring Franklin Jones presenting on Black American Sign Language (BASL).',
|
||||
slug: 'nobda-lad-basl-book-reading',
|
||||
featured: true,
|
||||
},
|
||||
{
|
||||
title: 'LSDF 19th Annual Dash for Deaf Kids 5K',
|
||||
date: 'April 15, 2023',
|
||||
location: 'Louisiana',
|
||||
description:
|
||||
'LAD-sponsored annual 5K run/walk benefiting the Louisiana School for the Deaf Foundation\'s programs for deaf youth.',
|
||||
slug: 'dash-for-deaf-kids-2023',
|
||||
},
|
||||
{
|
||||
title: 'LAD Quarterly Board Meeting',
|
||||
date: 'January 21, 2023',
|
||||
location: 'Rapides Parish Main Library, Alexandria, LA',
|
||||
description:
|
||||
'Quarterly Board Meeting open to all LAD members.',
|
||||
slug: 'quarterly-board-meeting-jan-2023',
|
||||
},
|
||||
{
|
||||
title: 'LAD Quarterly Meeting',
|
||||
date: 'April 1, 2023',
|
||||
location: 'Deaf Action Center, Shreveport, LA',
|
||||
description: 'Quarterly general membership meeting.',
|
||||
slug: 'quarterly-meeting-apr-2023',
|
||||
},
|
||||
{
|
||||
title: 'Meet Deaf Santa',
|
||||
date: 'December 11, 2022',
|
||||
location: 'LAD Office',
|
||||
description:
|
||||
'Annual holiday event for deaf children — meet Deaf Santa, games, and holiday celebration.',
|
||||
slug: 'meet-deaf-santa-2022',
|
||||
},
|
||||
{
|
||||
title: 'Deaf Seniors Luncheon',
|
||||
date: 'December 6, 2022',
|
||||
location: "Mike Anderson's, Gonzales, LA",
|
||||
description: 'Annual luncheon celebrating deaf seniors in our community.',
|
||||
slug: 'deaf-seniors-luncheon-2022',
|
||||
},
|
||||
{
|
||||
title: 'LAD Happy Hour Social',
|
||||
date: 'January 22, 2020',
|
||||
location: 'Tin Roof Brewing, Baton Rouge',
|
||||
description: 'Casual social gathering for LAD members and the broader Deaf community.',
|
||||
slug: 'happy-hour-2020',
|
||||
},
|
||||
{
|
||||
title: 'New Year Workshop with Gary W. Olsen',
|
||||
date: 'January 11, 2020',
|
||||
location: 'Louisiana School for the Deaf',
|
||||
description:
|
||||
'Free workshop for all LAD members featuring Gary W. Olsen, former President of the National Association of the Deaf.',
|
||||
slug: 'new-year-workshop-2020',
|
||||
},
|
||||
{
|
||||
title: 'Deaf Focus 10th Anniversary Gala',
|
||||
date: 'October 5, 2019',
|
||||
location: 'Baton Rouge',
|
||||
description:
|
||||
'Celebrating 10 years of Deaf Focus with special guests John Maucere and DJ Nico DiMarco.',
|
||||
slug: 'deaf-focus-10th-anniversary',
|
||||
},
|
||||
{
|
||||
title: 'Monthly ASL Meet & Greet',
|
||||
date: 'Last Thursday of each month (except Nov/Dec)',
|
||||
location: "Torchy's Tacos, Baton Rouge",
|
||||
description:
|
||||
'Monthly social gathering — ASL practice, networking, and community. All skill levels welcome. Check Facebook for current location updates.',
|
||||
slug: 'asl-meet-and-greet',
|
||||
},
|
||||
{
|
||||
title: 'LAD Annual Crawfish Boil',
|
||||
date: 'March 8, 2020',
|
||||
location: 'Louisiana',
|
||||
description: 'First annual LAD Crawfish Boil — proceeds support the LAD Youth Fund.',
|
||||
slug: 'crawfish-boil-2020',
|
||||
},
|
||||
];
|
||||
async function getEvents(): Promise<Event[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.BACKEND_URL}/api/events?status=published&limit=100`,
|
||||
{ next: { revalidate: 3600 } }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.events ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const [year, month, day] = dateStr.split('T')[0].split('-').map(Number);
|
||||
return new Date(year, month - 1, day).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
});
|
||||
}
|
||||
|
||||
const EventCard = ({ event }: { event: Event }) => (
|
||||
<div className={`card event-card-gold-border h-full flex flex-col ${event.featured ? 'ring-2 ring-[var(--color-primary)]' : ''}`}>
|
||||
<div className="card event-card-gold-border h-full flex flex-col">
|
||||
<div className="p-6 flex-grow flex flex-col">
|
||||
{event.featured && (
|
||||
<span className="featured-badge self-start mb-3 px-2 py-0.5 rounded-full text-xs">
|
||||
Featured
|
||||
</span>
|
||||
)}
|
||||
<h3 className="card-title event-card-title-gold text-xl font-semibold mb-2">{event.title}</h3>
|
||||
<p className="card-date font-medium text-sm mb-1">{event.date}</p>
|
||||
<p className="card-date font-medium text-sm mb-1">{formatDate(event.date)}{event.time ? ` · ${event.time}` : ''}</p>
|
||||
<p className="section-text text-sm mb-3">{event.location}</p>
|
||||
<p className="card-description mb-4 flex-grow">{event.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
export default function EventsPage() {
|
||||
const featuredEvents = events.filter((e) => e.featured);
|
||||
const upcomingEvents = events.filter((e) => !e.featured);
|
||||
export default async function EventsPage() {
|
||||
const events = await getEvents();
|
||||
const now = new Date();
|
||||
const upcoming = events.filter((e) => new Date(e.date) >= now);
|
||||
const past = events.filter((e) => new Date(e.date) < now);
|
||||
|
||||
return (
|
||||
<div className="bg-color-bg">
|
||||
|
|
@ -147,31 +71,43 @@ export default function EventsPage() {
|
|||
</div>
|
||||
</section>
|
||||
|
||||
{/* Featured events */}
|
||||
{featuredEvents.length > 0 && (
|
||||
{/* Upcoming events */}
|
||||
{upcoming.length > 0 && (
|
||||
<section className="py-12 bg-color-bg">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="section-heading text-3xl font-bold mb-8 text-center">Featured Events</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 max-w-4xl mx-auto">
|
||||
{featuredEvents.map((event) => (
|
||||
<EventCard key={event.slug} event={event} />
|
||||
<h2 className="section-heading text-3xl font-bold mb-8 text-center">Upcoming Events</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 max-w-6xl mx-auto">
|
||||
{upcoming.map((event) => (
|
||||
<EventCard key={event._id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* All events */}
|
||||
<section className="py-12 bg-color-bg-secondary">
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="section-heading text-3xl font-bold mb-8 text-center">All Events</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{upcomingEvents.map((event) => (
|
||||
<EventCard key={event.slug} event={event} />
|
||||
))}
|
||||
{/* Past events */}
|
||||
{past.length > 0 && (
|
||||
<section className={`py-12 ${upcoming.length > 0 ? 'bg-color-bg-secondary' : 'bg-color-bg'}`}>
|
||||
<div className="container mx-auto px-4">
|
||||
<h2 className="section-heading text-3xl font-bold mb-8 text-center">
|
||||
{upcoming.length === 0 ? 'All Events' : 'Past Events'}
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||
{past.map((event) => (
|
||||
<EventCard key={event._id} event={event} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{events.length === 0 && (
|
||||
<section className="py-16 bg-color-bg">
|
||||
<div className="container mx-auto px-4 text-center">
|
||||
<p className="section-text py-12">No events available at this time.</p>
|
||||
</div>
|
||||
</section>
|
||||
)}
|
||||
|
||||
{/* Event categories */}
|
||||
<section className="py-12 bg-color-bg">
|
||||
|
|
|
|||
|
|
@ -7,53 +7,65 @@ export const metadata: Metadata = {
|
|||
description: 'Official meeting minutes of the Louisiana Association of the Deaf.',
|
||||
};
|
||||
|
||||
type MinuteType =
|
||||
| 'Quarterly Meeting'
|
||||
| 'Board Meeting'
|
||||
| 'Committee Meeting'
|
||||
| 'Conference Minutes'
|
||||
| 'Financial Report'
|
||||
| 'Special Meeting';
|
||||
|
||||
interface MinuteEntry {
|
||||
date: string;
|
||||
type: MinuteType;
|
||||
label: string;
|
||||
membersOnly: boolean;
|
||||
interface DocRecord {
|
||||
_id: string;
|
||||
title: string;
|
||||
documentType: string;
|
||||
isPublic: boolean;
|
||||
meetingDate?: string;
|
||||
filePath: string;
|
||||
}
|
||||
|
||||
const minutes: MinuteEntry[] = [
|
||||
{ date: '2022-06-07', type: 'Quarterly Meeting', label: 'June 2022 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2022-03-01', type: 'Board Meeting', label: 'March 2022 Board Meeting', membersOnly: true },
|
||||
{ date: '2021-12-01', type: 'Quarterly Meeting', label: 'December 2021 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2021-09-01', type: 'Quarterly Meeting', label: 'September 2021 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2021-06-01', type: 'Quarterly Meeting', label: 'June 2021 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2021-03-01', type: 'Board Meeting', label: 'March 2021 Board Meeting', membersOnly: true },
|
||||
{ date: '2020-12-01', type: 'Quarterly Meeting', label: 'December 2020 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2020-09-01', type: 'Quarterly Meeting', label: 'September 2020 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2020-06-01', type: 'Board Meeting', label: 'June 2020 Board Meeting', membersOnly: true },
|
||||
{ date: '2020-03-01', type: 'Quarterly Meeting', label: 'March 2020 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2019-12-01', type: 'Quarterly Meeting', label: 'December 2019 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2019-09-01', type: 'Quarterly Meeting', label: 'September 2019 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2019-06-01', type: 'Conference Minutes', label: '2019 LAD Biennial Conference Minutes', membersOnly: false },
|
||||
{ date: '2019-03-01', type: 'Quarterly Meeting', label: 'March 2019 Quarterly Meeting', membersOnly: false },
|
||||
{ date: '2017-06-15', type: 'Conference Minutes', label: '50th Biennial Conference Minutes (June 2017)', membersOnly: false },
|
||||
{ date: '2016-01-01', type: 'Special Meeting', label: '2016 Special Meeting', membersOnly: true },
|
||||
{ date: '2015-01-01', type: 'Committee Meeting', label: '2015 Committee Meeting', membersOnly: true },
|
||||
{ date: '2014-01-01', type: 'Financial Report', label: '2014 Financial Report', membersOnly: true },
|
||||
];
|
||||
async function getDocuments(): Promise<DocRecord[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.BACKEND_URL}/api/documents?limit=500`,
|
||||
{ next: { revalidate: 3600 } }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.documents ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
const typeBadgeColor: Record<MinuteType, string> = {
|
||||
'Quarterly Meeting': 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
'Board Meeting': 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
'Committee Meeting': 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
'Conference Minutes': 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
'Financial Report': 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
'Special Meeting': 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
const docTypeLabel: Record<string, string> = {
|
||||
meeting_minutes: 'Meeting Minutes',
|
||||
board_meeting_minutes: 'Board Meeting',
|
||||
committee_meeting_minutes: 'Committee Meeting',
|
||||
annual_meeting_minutes: 'Annual Meeting',
|
||||
quarterly_meeting_minutes: 'Quarterly Meeting',
|
||||
conference_minutes: 'Conference Minutes',
|
||||
special_meeting_minutes: 'Special Meeting',
|
||||
bylaws: 'Bylaws',
|
||||
financial_report: 'Financial Report',
|
||||
annual_report: 'Annual Report',
|
||||
board_report: 'Board Report',
|
||||
committee_report: 'Committee Report',
|
||||
meeting_agenda: 'Meeting Agenda',
|
||||
board_meeting_agenda: 'Board Agenda',
|
||||
committee_meeting_agenda: 'Committee Agenda',
|
||||
program_document: 'Program Document',
|
||||
};
|
||||
|
||||
const docTypeBadge: Record<string, string> = {
|
||||
meeting_minutes: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
quarterly_meeting_minutes: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
annual_meeting_minutes: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-200',
|
||||
board_meeting_minutes: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-200',
|
||||
committee_meeting_minutes: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200',
|
||||
conference_minutes: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200',
|
||||
special_meeting_minutes: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200',
|
||||
financial_report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
annual_report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
board_report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
committee_report: 'bg-orange-100 text-orange-800 dark:bg-orange-900 dark:text-orange-200',
|
||||
};
|
||||
const defaultBadge = 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-200';
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
const [year, month, day] = dateStr.split('T')[0].split('-').map(Number);
|
||||
return new Date(year, month - 1, day).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
|
@ -61,9 +73,17 @@ function formatDate(dateStr: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
export default function MinutesPage() {
|
||||
const publicMinutes = minutes.filter((m) => !m.membersOnly);
|
||||
const memberMinutes = minutes.filter((m) => m.membersOnly);
|
||||
export default async function MinutesPage() {
|
||||
const docs = await getDocuments();
|
||||
const publicDocs = docs
|
||||
.filter((d) => d.isPublic)
|
||||
.sort((a, b) => {
|
||||
const da = a.meetingDate ?? a._id;
|
||||
const db = b.meetingDate ?? b._id;
|
||||
return da > db ? -1 : 1;
|
||||
});
|
||||
const memberCount = docs.filter((d) => !d.isPublic).length;
|
||||
const backendUrl = process.env.NEXT_PUBLIC_BACKEND_URL ?? '';
|
||||
|
||||
return (
|
||||
<div className="bg-color-bg">
|
||||
|
|
@ -87,26 +107,44 @@ export default function MinutesPage() {
|
|||
<p className="section-text mb-8">
|
||||
Quarterly meeting minutes and conference minutes are available to the public.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{publicMinutes.map((minute, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="card event-card-gold-border flex items-center justify-between px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<span
|
||||
className={`text-xs font-semibold px-2 py-1 rounded-full ${typeBadgeColor[minute.type]}`}
|
||||
>
|
||||
{minute.type}
|
||||
</span>
|
||||
<span className="card-title font-medium">{minute.label}</span>
|
||||
{publicDocs.length === 0 ? (
|
||||
<p className="section-text text-center py-8">No public documents available at this time.</p>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{publicDocs.map((doc) => (
|
||||
<div
|
||||
key={doc._id}
|
||||
className="card event-card-gold-border flex items-center justify-between px-6 py-4"
|
||||
>
|
||||
<div className="flex items-center gap-4 min-w-0">
|
||||
<span
|
||||
className={`text-xs font-semibold px-2 py-1 rounded-full shrink-0 ${docTypeBadge[doc.documentType] ?? defaultBadge}`}
|
||||
>
|
||||
{docTypeLabel[doc.documentType] ?? doc.documentType}
|
||||
</span>
|
||||
<span className="card-title font-medium truncate">{doc.title}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 ml-4 shrink-0">
|
||||
{doc.meetingDate && (
|
||||
<span className="card-date text-sm whitespace-nowrap hidden sm:block">
|
||||
{formatDate(doc.meetingDate)}
|
||||
</span>
|
||||
)}
|
||||
{doc.filePath && backendUrl && (
|
||||
<a
|
||||
href={`${backendUrl}/${doc.filePath}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="card-link text-sm font-medium underline whitespace-nowrap"
|
||||
>
|
||||
Download PDF
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<span className="card-date text-sm whitespace-nowrap ml-4">
|
||||
{formatDate(minute.date)}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
|
@ -119,8 +157,10 @@ export default function MinutesPage() {
|
|||
<div className="card event-card-gold-border p-8">
|
||||
<p className="section-text text-lg mb-6">
|
||||
Board meeting minutes, committee meeting minutes, and financial reports are available
|
||||
exclusively to current LAD members. These records cover {memberMinutes.length} additional
|
||||
documents dating back to 2005.
|
||||
exclusively to current LAD members.
|
||||
{memberCount > 0 && (
|
||||
<> These records cover {memberCount} additional documents dating back to 2005.</>
|
||||
)}
|
||||
</p>
|
||||
<Link href="/membership" className="btn-primary inline-block">
|
||||
Become a Member for Full Access
|
||||
|
|
|
|||
|
|
@ -8,81 +8,28 @@ export const metadata: Metadata = {
|
|||
};
|
||||
|
||||
interface Update {
|
||||
_id: string;
|
||||
date: string;
|
||||
title: string;
|
||||
body: string;
|
||||
}
|
||||
|
||||
const updates: Update[] = [
|
||||
{
|
||||
date: '2022-07-05',
|
||||
title: 'Governor Edwards Signs Act 128 Into Law',
|
||||
body: 'Governor John Bel Edwards has signed Act 128 into law, adding a DeafBlind representative to the Louisiana Commission for the Deaf — a major advocacy victory for the DeafBlind community in Louisiana.',
|
||||
},
|
||||
{
|
||||
date: '2022-07-02',
|
||||
title: 'LAD Board Attends NAD 56th Biennial Conference in Orlando',
|
||||
body: 'The LAD Board of Directors attended the National Association of the Deaf\'s 56th Biennial Conference in Orlando, Florida, representing Louisiana\'s deaf community on the national stage.',
|
||||
},
|
||||
{
|
||||
date: '2019-12-04',
|
||||
title: 'New Year Event with Gary W. Olsen, Former NAD President',
|
||||
body: 'LAD is hosting a special New Year Workshop with Gary W. Olsen, former President of the National Association of the Deaf, on January 11, 2020 at LSD. Free for all LAD members.',
|
||||
},
|
||||
{
|
||||
date: '2019-11-22',
|
||||
title: 'Marc Bartholomew Appointed Interim Treasurer',
|
||||
body: 'The LAD Board has appointed Marc Bartholomew as Interim Treasurer.',
|
||||
},
|
||||
{
|
||||
date: '2019-10-11',
|
||||
title: 'LCD Director Ariel Bumbala Resigns; Jana Broussard Named Interim Director',
|
||||
body: 'Louisiana Commission for the Deaf Director Ariel Bumbala has resigned. Jana Broussard has been named Interim Director of the LCD.',
|
||||
},
|
||||
{
|
||||
date: '2019-06-08',
|
||||
title: '2019–2021 LAD Board Elected',
|
||||
body: 'The newly elected 2019–2021 LAD Board: President Jimmy Challis Gore, Vice President Sherry Crosby, Secretary Jay Isch, Treasurer Alla Tarasyuk.',
|
||||
},
|
||||
{
|
||||
date: '2017-07-20',
|
||||
title: 'Three YLC Campers Sent to NAD Youth Leadership Camp',
|
||||
body: 'LAD proudly sponsored three Young Leaders campers to attend the NAD Youth Leadership Camp, investing in the next generation of Deaf leaders in Louisiana.',
|
||||
},
|
||||
{
|
||||
date: '2017-01-05',
|
||||
title: 'LAD 50th Biennial Conference Announced — June 15–17, 2017',
|
||||
body: 'The Louisiana Association of the Deaf announces its 50th Biennial Conference scheduled for June 15–17, 2017. Members and the broader Deaf community are encouraged to attend.',
|
||||
},
|
||||
{
|
||||
date: '2016-04-05',
|
||||
title: '1939 Louisiana School for the Deaf Time Capsule Opened',
|
||||
body: 'A time capsule buried in 1939 at the Louisiana School for the Deaf was opened, offering a remarkable historical glimpse into Deaf education and community life in Louisiana.',
|
||||
},
|
||||
{
|
||||
date: '2016-03-28',
|
||||
title: 'LAD Endorses Deaf Grassroots Movement of Louisiana (DGM-LA)',
|
||||
body: 'The Louisiana Association of the Deaf has officially endorsed the Deaf Grassroots Movement of Louisiana (DGM-LA), supporting grassroots advocacy across the state.',
|
||||
},
|
||||
{
|
||||
date: '2015-05-26',
|
||||
title: 'Press Release: LAD vs. Lisa Chase Potter — Records Access Litigation',
|
||||
body: 'LAD has released a statement regarding ongoing records access litigation (LAD vs. Lisa Chase Potter). The organization remains committed to transparency and member rights.',
|
||||
},
|
||||
{
|
||||
date: '2014-08-08',
|
||||
title: 'Deaf Driver Self-ID System Added to Louisiana DMV Database',
|
||||
body: 'A public safety self-identification system for deaf drivers has been added to the Louisiana DMV database, improving safety for deaf individuals during traffic stops.',
|
||||
},
|
||||
{
|
||||
date: '2014-08-04',
|
||||
title: 'Class Action Lawsuit — Louisiana Relay Service',
|
||||
body: 'LAD is party to a class action lawsuit regarding the Louisiana Relay Service, advocating for improved telecommunications access for the deaf and hard-of-hearing community.',
|
||||
},
|
||||
];
|
||||
async function getUpdates(): Promise<Update[]> {
|
||||
try {
|
||||
const res = await fetch(
|
||||
`${process.env.BACKEND_URL}/api/updates?status=published&limit=100`,
|
||||
{ next: { revalidate: 3600 } }
|
||||
);
|
||||
if (!res.ok) return [];
|
||||
const data = await res.json();
|
||||
return data.updates ?? [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string): string {
|
||||
const [year, month, day] = dateStr.split('-').map(Number);
|
||||
const [year, month, day] = dateStr.split('T')[0].split('-').map(Number);
|
||||
return new Date(year, month - 1, day).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
|
|
@ -90,7 +37,9 @@ function formatDate(dateStr: string): string {
|
|||
});
|
||||
}
|
||||
|
||||
export default function UpdatesPage() {
|
||||
export default async function UpdatesPage() {
|
||||
const updates = await getUpdates();
|
||||
|
||||
return (
|
||||
<div className="bg-color-bg">
|
||||
{/* Hero */}
|
||||
|
|
@ -109,15 +58,19 @@ export default function UpdatesPage() {
|
|||
<section className="py-16 bg-color-bg">
|
||||
<div className="container mx-auto px-4">
|
||||
<div className="max-w-4xl mx-auto space-y-6">
|
||||
{updates.map((update, i) => (
|
||||
<article key={i} className="card event-card-gold-border p-6 md:p-8">
|
||||
<p className="card-date text-sm font-medium mb-2">{formatDate(update.date)}</p>
|
||||
<h2 className="card-title event-card-title-gold text-xl font-semibold mb-3">
|
||||
{update.title}
|
||||
</h2>
|
||||
<p className="section-text">{update.body}</p>
|
||||
</article>
|
||||
))}
|
||||
{updates.length === 0 ? (
|
||||
<p className="section-text text-center py-12">No updates available at this time.</p>
|
||||
) : (
|
||||
updates.map((update) => (
|
||||
<article key={update._id} className="card event-card-gold-border p-6 md:p-8">
|
||||
<p className="card-date text-sm font-medium mb-2">{formatDate(update.date)}</p>
|
||||
<h2 className="card-title event-card-title-gold text-xl font-semibold mb-3">
|
||||
{update.title}
|
||||
</h2>
|
||||
<p className="section-text">{update.body}</p>
|
||||
</article>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="max-w-4xl mx-auto mt-12 text-center">
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue