docs: add ACCESSIBILITY_AUDIT, SECURITY_PRIVACY_AUDIT, PRODUCT_BRIEF with file-specific actions; a11y: add skip link, lang+hreflang, ARIA for menu/carousel/dialog/form; noValidate+honeypot
This commit is contained in:
148
ACCESSIBILITY_AUDIT.md
Normal file
148
ACCESSIBILITY_AUDIT.md
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
### Omoluabi Accessibility Audit (WCAG 2.2 AA)
|
||||||
|
|
||||||
|
Scope: Current Astro site as of this commit. Focus on perceivable, operable, understandable, and robust criteria for Nigerians in NL and Dutch partners.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Executive summary
|
||||||
|
- **Overall**: Solid semantic base with clear landmarks in `src/layouts/BaseLayout.astro`. Reduced-motion support exists in `src/styles/global.css`. Key gaps: language/i18n markup, skip links, menu ARIA, carousel semantics, form error/ARIA, dialog semantics, and strict CSP readiness.
|
||||||
|
- **Risk**: Medium → High for navigation/keyboard and language; Low → Medium for contrast on image/gradient backgrounds.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### High-priority issues (fix now)
|
||||||
|
|
||||||
|
1) Missing skip link + focus target
|
||||||
|
- File: `src/layouts/BaseLayout.astro`
|
||||||
|
- Snippet (add just inside `<body>` and set `id` on `<main>`):
|
||||||
|
```astro
|
||||||
|
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-black focus:px-3 focus:py-2 focus:rounded">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
|
...
|
||||||
|
<main id="main" tabindex="-1">
|
||||||
|
<slot />
|
||||||
|
</main>
|
||||||
|
```
|
||||||
|
- Rationale: Allows keyboard users to bypass repeated navigation (WCAG 2.4.1, 2.4.3).
|
||||||
|
|
||||||
|
2) Page language + `hreflang`
|
||||||
|
- File: `src/layouts/BaseLayout.astro`
|
||||||
|
- Snippet (top `<html>` and `<head>`):
|
||||||
|
```astro
|
||||||
|
<html lang={Astro.props.lang ?? 'en'}>
|
||||||
|
<head>
|
||||||
|
...
|
||||||
|
<link rel="alternate" hrefLang="en" href="/en/" />
|
||||||
|
<link rel="alternate" hrefLang="nl" href="/nl/" />
|
||||||
|
<link rel="alternate" hrefLang="x-default" href="/" />
|
||||||
|
```
|
||||||
|
- Rationale: Correct language for screen readers and localized SEO (WCAG 3.1.1, 3.1.2).
|
||||||
|
|
||||||
|
3) Mobile menu button lacks ARIA state wiring
|
||||||
|
- File: `src/layouts/BaseLayout.astro`
|
||||||
|
- Snippet (button + script):
|
||||||
|
```astro
|
||||||
|
<button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
|
||||||
|
...
|
||||||
|
</button>
|
||||||
|
```
|
||||||
|
```html
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
if (mobileMenuButton && mobileMenu) {
|
||||||
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
|
const isHidden = mobileMenu.classList.toggle('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', String(!isHidden));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
```
|
||||||
|
- Rationale: Communicates expanded/collapsed state for assistive tech (WCAG 4.1.2).
|
||||||
|
|
||||||
|
4) Carousel semantics and controls
|
||||||
|
- File: `src/components/HeroCarousel.jsx`
|
||||||
|
- Snippet:
|
||||||
|
```jsx
|
||||||
|
<div
|
||||||
|
className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"
|
||||||
|
role="region"
|
||||||
|
aria-roledescription="carousel"
|
||||||
|
aria-label="Featured content"
|
||||||
|
>
|
||||||
|
{slides.map((slide, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
aria-label={`${index + 1} of ${slides.length}`}
|
||||||
|
className={...}
|
||||||
|
>
|
||||||
|
...
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<button aria-pressed={!isPlaying} aria-label={isPlaying ? 'Pause slideshow' : 'Play slideshow'}>...</button>
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
- Rationale: Conveys carousel/slide semantics; exposes play/pause state (WCAG 1.4.2, 2.2.2, 4.1.2).
|
||||||
|
|
||||||
|
5) Contact form ARIA and honeypot
|
||||||
|
- File: `src/components/ContactForm.jsx`
|
||||||
|
- Snippet:
|
||||||
|
```jsx
|
||||||
|
const [hp, setHp] = useState('');
|
||||||
|
...
|
||||||
|
<form onSubmit={handleSubmit} noValidate ...>
|
||||||
|
<div className="hidden" aria-hidden="true">
|
||||||
|
<label htmlFor="website">Website</label>
|
||||||
|
<input id="website" name="website" value={hp} onChange={(e)=>setHp(e.target.value)} tabIndex={-1} autoComplete="off" />
|
||||||
|
</div>
|
||||||
|
<input aria-invalid={Boolean(errors.name)} aria-describedby={errors.name ? 'name-error' : undefined} ... />
|
||||||
|
{errors.name && <p id="name-error" className="...">{errors.name}</p>}
|
||||||
|
```
|
||||||
|
- Rationale: Accessible errors association and simple anti-bot (WCAG 3.3.1/3.3.3).
|
||||||
|
|
||||||
|
6) Lightbox dialog semantics
|
||||||
|
- File: `src/components/Lightbox.jsx`
|
||||||
|
- Snippet:
|
||||||
|
```jsx
|
||||||
|
<div
|
||||||
|
className="fixed inset-0 ..."
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-label="Image viewer"
|
||||||
|
onClick={onClose}
|
||||||
|
>
|
||||||
|
```
|
||||||
|
- Rationale: Dialog semantics and modality (WCAG 1.3.1, 2.4.3, 4.1.2).
|
||||||
|
|
||||||
|
7) Inline styles and data-URI backgrounds limit strict CSP
|
||||||
|
- Files: `src/pages/about.astro`, `src/pages/donate.astro`, `src/pages/orphanage.astro`
|
||||||
|
- Fix: Extract inline `style="background: ..."` to named CSS classes in `src/styles/main.css` (e.g., `.bg-flag-nl-ng`, `.pattern-overlay`) and reference via `class` only. Move inline `<script>` in layout to a module file and import with `type="module"` and `nonce`.
|
||||||
|
- Rationale: Enables strict non-`unsafe-inline` CSP (security and robustness).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Medium-priority issues
|
||||||
|
- **Contrast over imagery/gradients**: Ensure minimum overlay like `bg-black/60` behind text; avoid fully transparent gradient text for body copy. Files: `src/pages/index.astro`, `src/pages/about.astro`, `HeroCarousel.jsx`.
|
||||||
|
- **Focus visibility baseline**: Add base `:focus-visible` style in `src/styles/global.css` to ensure consistent visible focus.
|
||||||
|
- **Image dimensions**: Add `width`/`height` on `<img>` where known to prevent CLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Suggested global utilities
|
||||||
|
- File: `src/styles/global.css`
|
||||||
|
```css
|
||||||
|
:focus-visible { outline: 2px solid var(--nigerian-green); outline-offset: 2px; }
|
||||||
|
[role="button"], a, button, input, select, textarea { -webkit-tap-highlight-color: transparent; }
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Verification plan (to add in CI)
|
||||||
|
- Playwright + `@axe-core/playwright`: nav landmarks, skip link focus, keyboard-only menu, form labels/errors, dialog semantics.
|
||||||
|
- Lighthouse budgets: LCP < 2.0s, CLS < 0.05, TTI < 3.5s; run on PR and fail if exceeded.
|
||||||
|
- Spot-check color contrast with axe; prohibit text over images without overlay.
|
42
PRODUCT_BRIEF.md
Normal file
42
PRODUCT_BRIEF.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
## Product Brief — Omoluabi (NL)
|
||||||
|
|
||||||
|
### Audience
|
||||||
|
- Nigerians living in the Netherlands (families, youth, professionals)
|
||||||
|
- Dutch institutions and partners
|
||||||
|
- Donors and volunteers
|
||||||
|
|
||||||
|
### Objectives
|
||||||
|
- Community growth and engagement (events, programs, volunteering)
|
||||||
|
- Cultural education and representation
|
||||||
|
- Fundraising for programs and orphanage
|
||||||
|
- Trust through accessibility, privacy, and transparency
|
||||||
|
|
||||||
|
### KPIs / Success metrics
|
||||||
|
- Monthly active visitors (MAU) and returning visitors
|
||||||
|
- Event registrations and attendance rate
|
||||||
|
- Volunteer sign-ups and inquiries
|
||||||
|
- Donations volume via iDEAL/cards (Mollie)
|
||||||
|
- Newsletter subscriptions (double opt-in)
|
||||||
|
- Accessibility: axe violations = 0 on core templates; Lighthouse: LCP < 2.0s, CLS < 0.05
|
||||||
|
|
||||||
|
### Value proposition
|
||||||
|
A fast, inclusive, bilingual (EN/NL) hub connecting the Nigerian diaspora and Dutch partners, showcasing culture, programs, and ways to contribute—built privacy-first and accessible by default.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Gap report (current site)
|
||||||
|
|
||||||
|
- **Internationalization**: No locale-aware routing or page `lang`; no language switcher. Impact: accessibility, SEO, Dutch partners.
|
||||||
|
- **Accessibility**: Missing skip links; inconsistent focus; carousel lacks semantics; form errors lack ARIA; dialog lacks `role="dialog"`. Impact: keyboard and screen reader users.
|
||||||
|
- **Security/Privacy**: Google Fonts import; inline scripts/styles block strict CSP; no headers/CSP; forms not server-validated; no anti-abuse. Impact: GDPR risk and security posture.
|
||||||
|
- **Donations**: UI-only progress bar, no payments backend; no iDEAL/cards. Impact: fundraising.
|
||||||
|
- **Analytics**: None; decision pending (Plausible/Matomo). Impact: insight and optimization.
|
||||||
|
- **Content model**: Content lives in Markdown ad-hoc; lacks structured collections for posts/programs/team/partners; no Zod schemas.
|
||||||
|
- **Images/Performance**: Static `<img>` without `width/height` and `<picture>`; risky contrast over images; potential CLS.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Prioritized roadmap (Now/Next/Later)
|
||||||
|
- **Now (this sprint)**: Implement headers/CSP-ready refactors, a11y fixes (skip link, menu ARIA, carousel/dialog semantics, focus baseline), self-host fonts, scaffold content collections with Zod, propose IA and i18n routing.
|
||||||
|
- **Next**: Implement i18n routes (`/en/`, `/nl/`), language switcher, interactive events calendar with filters and ICS export, server forms (contact/volunteer/partner), Plausible EU.
|
||||||
|
- **Later**: Mollie donation flow, Leaflet map for events/partners, PWA, program directory, newsletter double opt-in, Lighthouse/axe CI.
|
121
SECURITY_PRIVACY_AUDIT.md
Normal file
121
SECURITY_PRIVACY_AUDIT.md
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
### Security & Privacy Audit (EU/GDPR)
|
||||||
|
|
||||||
|
Scope: Current Astro site; no server endpoints yet; React islands used. Aim: strict security headers, EU analytics, consent only if needed, secrets hygiene, safe forms, and payment integration readiness.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Executive summary
|
||||||
|
- **Strengths**: Static-first Astro; no server auth; minimal third parties.
|
||||||
|
- **Gaps**: External Google Fonts (tracking and CSP issues), inline scripts/styles, no security headers configured, forms lack server validation/rate-limiting/CSRF, no cookie/consent model, no analytics choice, donation flow not integrated with EU processor.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Action checklist (priority)
|
||||||
|
|
||||||
|
1) Remove Google Fonts; self-host Inter variable
|
||||||
|
- File: `src/styles/global.css`
|
||||||
|
- Replace `@import` from Google with self-hosted font. Steps:
|
||||||
|
- Add `public/fonts/inter/Inter-Variable.woff2`.
|
||||||
|
- Add to `src/styles/global.css`:
|
||||||
|
```css
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Inter';
|
||||||
|
src: url('/fonts/inter/Inter-Variable.woff2') format('woff2-variations');
|
||||||
|
font-weight: 100 900; font-style: normal; font-display: swap;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- Rationale: Privacy, performance, CSP friendliness.
|
||||||
|
|
||||||
|
2) Security headers
|
||||||
|
- Option A: Static hosting `_headers` (e.g., Netlify/Vercel):
|
||||||
|
```text
|
||||||
|
/*
|
||||||
|
Strict-Transport-Security: max-age=31536000; includeSubDomains; preload
|
||||||
|
X-Content-Type-Options: nosniff
|
||||||
|
Referrer-Policy: strict-origin-when-cross-origin
|
||||||
|
Permissions-Policy: geolocation=(), microphone=(), camera=(), payment=()
|
||||||
|
Cross-Origin-Opener-Policy: same-origin
|
||||||
|
Cross-Origin-Embedder-Policy: require-corp; report-to="default"
|
||||||
|
Cross-Origin-Resource-Policy: same-origin
|
||||||
|
Content-Security-Policy: default-src 'self'; script-src 'self' 'nonce-PLACEHOLDER'; style-src 'self' 'nonce-PLACEHOLDER'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'
|
||||||
|
```
|
||||||
|
- Option B: Nginx snippet (`nginx.conf.example`):
|
||||||
|
```nginx
|
||||||
|
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header Referrer-Policy "strict-origin-when-cross-origin";
|
||||||
|
add_header Permissions-Policy "geolocation=(), microphone=(), camera=(), payment=()";
|
||||||
|
add_header Cross-Origin-Opener-Policy "same-origin";
|
||||||
|
add_header Cross-Origin-Embedder-Policy "require-corp";
|
||||||
|
add_header Cross-Origin-Resource-Policy "same-origin";
|
||||||
|
add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'nonce-$request_id'; style-src 'self' 'nonce-$request_id'; img-src 'self' data:; font-src 'self' data:; connect-src 'self'; frame-ancestors 'none'; base-uri 'self'; form-action 'self'";
|
||||||
|
```
|
||||||
|
- Rationale: Baseline browser protections; strict CSP with nonces for inline modules.
|
||||||
|
|
||||||
|
3) Inline → external scripts/styles to satisfy CSP
|
||||||
|
- Files: `src/layouts/BaseLayout.astro` (inline `<script>`), several pages with inline `style="background..."`.
|
||||||
|
- Fix: Move JS to `src/scripts/navigation.ts` and import with `<script type="module" src="/scripts/navigation.js" nonce={Astro.props.nonce} />`; extract inline styles into classes in `src/styles/main.css`.
|
||||||
|
|
||||||
|
4) Forms: server validation, CSRF, rate-limit, honeypot
|
||||||
|
- Add endpoint example: `src/pages/api/contact.ts`
|
||||||
|
```ts
|
||||||
|
import type { APIRoute } from 'astro';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
const ContactSchema = z.object({
|
||||||
|
name: z.string().min(1),
|
||||||
|
email: z.string().email(),
|
||||||
|
subject: z.string().min(1),
|
||||||
|
message: z.string().min(1),
|
||||||
|
website: z.string().max(0).optional(), // honeypot
|
||||||
|
});
|
||||||
|
|
||||||
|
export const POST: APIRoute = async ({ request }) => {
|
||||||
|
try {
|
||||||
|
const data = await request.json();
|
||||||
|
const parsed = ContactSchema.safeParse(data);
|
||||||
|
if (!parsed.success) return new Response(JSON.stringify({ ok:false }), { status: 400 });
|
||||||
|
// TODO: rate-limit via IP + UA (e.g., in-memory or KV), CSRF via double-submit token header
|
||||||
|
// TODO: send via Postmark/Resend EU region
|
||||||
|
return new Response(JSON.stringify({ ok:true }), { status: 200 });
|
||||||
|
} catch {
|
||||||
|
return new Response(JSON.stringify({ ok:false }), { status: 400 });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
```
|
||||||
|
- Rationale: GDPR-friendly collection, safe error responses, anti-abuse measures.
|
||||||
|
|
||||||
|
5) Payments: Mollie integration plan
|
||||||
|
- Use server-only handlers for payment creation and webhook signature verification. Store nothing beyond required payment IDs; no card data. Configure allowlisted endpoints in CSP `connect-src` for Mollie API only when enabled.
|
||||||
|
|
||||||
|
6) Analytics: Plausible (EU) or Matomo (self-host)
|
||||||
|
- Recommendation: **Plausible EU** with cookieless, events limited to pageview + donate/volunteer conversions. If consent model needed later, implement prior to enabling cookies.
|
||||||
|
|
||||||
|
7) Secrets and environment hygiene
|
||||||
|
- Use `import.meta.env` and never expose secrets without `*_PUBLIC` prefix. Add `.env.example`:
|
||||||
|
```env
|
||||||
|
PUBLIC_SITE_URL=
|
||||||
|
NODE_ENV=development
|
||||||
|
# Analytics
|
||||||
|
PUBLIC_PLAUSIBLE_DOMAIN=
|
||||||
|
PUBLIC_PLAUSIBLE_SRC=
|
||||||
|
# Payments (server-only)
|
||||||
|
MOLLIE_API_KEY=
|
||||||
|
MOLLIE_WEBHOOK_SECRET=
|
||||||
|
# Email provider
|
||||||
|
POSTMARK_TOKEN=
|
||||||
|
```
|
||||||
|
|
||||||
|
8) Logging and retention
|
||||||
|
- Adopt structured logs without PII; error IDs returned to client; retain logs 30–90 days; no access logs with IP persisted beyond ops needs; document in Privacy Policy.
|
||||||
|
|
||||||
|
9) DPIA-lite
|
||||||
|
- Data categories: contact form (identifiers and message body), newsletter (email), donation (processor-hosted, no card data). Lawful basis: consent/legitimate interest. No special category data processed. Document processor agreements (Plausible EU, Postmark EU region, Mollie EU).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Next steps
|
||||||
|
- Implement headers and CSP; refactor inline code.
|
||||||
|
- Add API routes with Zod and anti-abuse; wire contact form to server.
|
||||||
|
- Choose analytics (Plausible EU) and configure opt-in text in footer.
|
||||||
|
- Prepare Mollie sandbox integration and webhook.
|
@@ -11,6 +11,7 @@ export default function ContactForm() {
|
|||||||
const [errors, setErrors] = useState({});
|
const [errors, setErrors] = useState({});
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
const [submitMessage, setSubmitMessage] = useState(null);
|
const [submitMessage, setSubmitMessage] = useState(null);
|
||||||
|
const [hp, setHp] = useState('');
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
let newErrors = {};
|
let newErrors = {};
|
||||||
@@ -61,7 +62,11 @@ export default function ContactForm() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg border border-gray-200 max-w-2xl mx-auto">
|
<form onSubmit={handleSubmit} className="bg-white p-8 rounded-xl shadow-lg border border-gray-200 max-w-2xl mx-auto" noValidate>
|
||||||
|
<div className="hidden" aria-hidden="true">
|
||||||
|
<label htmlFor="website">Website</label>
|
||||||
|
<input id="website" name="website" value={hp} onChange={(e)=>setHp(e.target.value)} tabIndex={-1} autoComplete="off" />
|
||||||
|
</div>
|
||||||
<h2 className="text-3xl font-bold text-center text-nigerian-green-700 mb-8">Send Us a Message</h2>
|
<h2 className="text-3xl font-bold text-center text-nigerian-green-700 mb-8">Send Us a Message</h2>
|
||||||
|
|
||||||
{submitMessage && (
|
{submitMessage && (
|
||||||
@@ -78,10 +83,12 @@ export default function ContactForm() {
|
|||||||
name="name"
|
name="name"
|
||||||
value={formData.name}
|
value={formData.name}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
|
aria-invalid={Boolean(errors.name)}
|
||||||
|
aria-describedby={errors.name ? 'name-error' : undefined}
|
||||||
className={`shadow appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-nigerian-green-500 ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
|
className={`shadow appearance-none border rounded-lg w-full py-3 px-4 text-gray-700 leading-tight focus:outline-none focus:ring-2 focus:ring-nigerian-green-500 ${errors.name ? 'border-red-500' : 'border-gray-300'}`}
|
||||||
placeholder="Your Name"
|
placeholder="Your Name"
|
||||||
/>
|
/>
|
||||||
{errors.name && <p className="text-red-500 text-xs italic mt-2">{errors.name}</p>}
|
{errors.name && <p id="name-error" className="text-red-500 text-xs italic mt-2">{errors.name}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-5">
|
<div className="mb-5">
|
||||||
|
@@ -78,7 +78,8 @@ export default function HeroCarousel() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white">
|
<div className="relative w-full max-w-6xl mx-auto rounded-2xl overflow-hidden shadow-2xl bg-white"
|
||||||
|
role="region" aria-roledescription="carousel" aria-label="Featured content">
|
||||||
{/* Main Carousel Container */}
|
{/* Main Carousel Container */}
|
||||||
<div className="relative h-[600px] overflow-hidden">
|
<div className="relative h-[600px] overflow-hidden">
|
||||||
{slides.map((slide, index) => {
|
{slides.map((slide, index) => {
|
||||||
@@ -93,6 +94,9 @@ export default function HeroCarousel() {
|
|||||||
? 'opacity-0 -translate-x-full'
|
? 'opacity-0 -translate-x-full'
|
||||||
: 'opacity-0 translate-x-full'
|
: 'opacity-0 translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
|
role="group"
|
||||||
|
aria-roledescription="slide"
|
||||||
|
aria-label={`${index + 1} of ${slides.length}`}
|
||||||
>
|
>
|
||||||
{/* Background Image */}
|
{/* Background Image */}
|
||||||
<div className="absolute inset-0">
|
<div className="absolute inset-0">
|
||||||
@@ -194,6 +198,7 @@ export default function HeroCarousel() {
|
|||||||
onClick={() => setIsPlaying(!isPlaying)}
|
onClick={() => setIsPlaying(!isPlaying)}
|
||||||
className="absolute top-4 right-4 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300 hover:scale-110"
|
className="absolute top-4 right-4 bg-white/20 backdrop-blur-sm hover:bg-white/30 text-white p-2 rounded-full transition-all duration-300 hover:scale-110"
|
||||||
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
|
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
|
||||||
|
aria-pressed={!isPlaying}
|
||||||
>
|
>
|
||||||
{isPlaying ? (
|
{isPlaying ? (
|
||||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||||
|
@@ -21,7 +21,8 @@ export default function Lightbox({ images, currentIndex, onClose, onNext, onPrev
|
|||||||
if (!images || images.length === 0) return null;
|
if (!images || images.length === 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="fixed inset-0 z-[9999] bg-black bg-opacity-90 flex items-center justify-center p-4" onClick={onClose}>
|
<div className="fixed inset-0 z-[9999] bg-black bg-opacity-90 flex items-center justify-center p-4" onClick={onClose}
|
||||||
|
role="dialog" aria-modal="true" aria-label="Image viewer">
|
||||||
<div className="relative max-w-5xl max-h-full" onClick={(e) => e.stopPropagation()}> {/* Prevent closing when clicking on image */}
|
<div className="relative max-w-5xl max-h-full" onClick={(e) => e.stopPropagation()}> {/* Prevent closing when clicking on image */}
|
||||||
<img
|
<img
|
||||||
src={images[currentIndex]}
|
src={images[currentIndex]}
|
||||||
|
@@ -1,26 +1,34 @@
|
|||||||
---
|
---
|
||||||
// src/layouts/BaseLayout.astro
|
// src/layouts/BaseLayout.astro
|
||||||
import '../styles/global.css';
|
import '../styles/global.css';
|
||||||
|
import '../styles/main.css';
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
title?: string;
|
title?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
lang?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props;
|
const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props;
|
||||||
---
|
---
|
||||||
|
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang={Astro.props.lang ?? 'en'}>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>{title}</title>
|
<title>{title}</title>
|
||||||
<meta name="description" content={description} />
|
<meta name="description" content={description} />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<link rel="alternate" hreflang="en" href="/en/" />
|
||||||
|
<link rel="alternate" hreflang="nl" href="/nl/" />
|
||||||
|
<link rel="alternate" hreflang="x-default" href="/" />
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body class="bg-gray-50">
|
<body class="bg-gray-50">
|
||||||
|
<a href="#main" class="sr-only focus:not-sr-only focus:fixed focus:top-2 focus:left-2 focus:z-50 focus:bg-white focus:text-black focus:px-3 focus:py-2 focus:rounded">
|
||||||
|
Skip to main content
|
||||||
|
</a>
|
||||||
<!-- Navigation Header -->
|
<!-- Navigation Header -->
|
||||||
<header class="bg-white shadow-sm sticky top-0 z-50">
|
<header class="bg-white shadow-sm sticky top-0 z-50">
|
||||||
<nav class="container mx-auto px-4 py-4">
|
<nav class="container mx-auto px-4 py-4">
|
||||||
@@ -58,7 +66,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Mobile menu button -->
|
<!-- Mobile menu button -->
|
||||||
<button class="md:hidden" id="mobile-menu-button">
|
<button class="md:hidden" id="mobile-menu-button" aria-controls="mobile-menu" aria-expanded="false">
|
||||||
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||||
</svg>
|
</svg>
|
||||||
@@ -85,7 +93,7 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Main Content -->
|
<!-- Main Content -->
|
||||||
<main>
|
<main id="main" tabindex="-1">
|
||||||
<slot />
|
<slot />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@@ -136,34 +144,22 @@ const { title = "Omoluabi Association Netherlands", description = "Preserving Ni
|
|||||||
|
|
||||||
<!-- Scripts -->
|
<!-- Scripts -->
|
||||||
<script>
|
<script>
|
||||||
// Mobile menu toggle
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
const mobileMenuButton = document.getElementById('mobile-menu-button');
|
||||||
const mobileMenu = document.getElementById('mobile-menu');
|
const mobileMenu = document.getElementById('mobile-menu');
|
||||||
|
|
||||||
if (mobileMenuButton && mobileMenu) {
|
if (mobileMenuButton && mobileMenu) {
|
||||||
mobileMenuButton.addEventListener('click', () => {
|
mobileMenuButton.addEventListener('click', () => {
|
||||||
mobileMenu.classList.toggle('hidden');
|
const isHidden = mobileMenu.classList.toggle('hidden');
|
||||||
|
mobileMenuButton.setAttribute('aria-expanded', String(!isHidden));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Intersection Observer for scroll animations
|
const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' };
|
||||||
const observerOptions = {
|
|
||||||
threshold: 0.1,
|
|
||||||
rootMargin: '0px 0px -50px 0px'
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach(entry => {
|
entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } });
|
||||||
if (entry.isIntersecting) {
|
|
||||||
entry.target.classList.add('visible');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, observerOptions);
|
}, observerOptions);
|
||||||
|
document.querySelectorAll('[data-animate-on-scroll]').forEach(el => observer.observe(el));
|
||||||
// Observe all elements with animation attributes
|
|
||||||
const animatedElements = document.querySelectorAll('[data-animate-on-scroll]');
|
|
||||||
animatedElements.forEach(el => observer.observe(el));
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
@@ -3,21 +3,93 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
---
|
---
|
||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header with Flag Integration -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden" style="
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
background:
|
||||||
|
linear-gradient(90deg, var(--nigerian-green) 0%, var(--nigerian-green) 33.33%, #fff 33.33%, #fff 66.66%, var(--nigerian-green) 66.66%, var(--nigerian-green) 100%),
|
||||||
|
linear-gradient(180deg, var(--dutch-red) 0%, var(--dutch-red) 33.33%, #fff 33.33%, #fff 66.66%, var(--dutch-blue) 66.66%, var(--dutch-blue) 100%);
|
||||||
|
background-size: 40% 100%, 40% 100%;
|
||||||
|
background-position: left, right;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
" data-animate-on-scroll="fade-in">
|
||||||
|
|
||||||
|
<!-- Flag Icons -->
|
||||||
|
<div class="absolute top-8 left-8 flex items-center gap-4 z-20">
|
||||||
|
<!-- Nigerian Flag -->
|
||||||
|
<div class="flex shadow-lg rounded-lg overflow-hidden border-2 border-white/20">
|
||||||
|
<div class="w-6 h-4 bg-nigerian-green-500"></div>
|
||||||
|
<div class="w-6 h-4 bg-white"></div>
|
||||||
|
<div class="w-6 h-4 bg-nigerian-green-500"></div>
|
||||||
|
</div>
|
||||||
|
<!-- Connection Symbol -->
|
||||||
|
<div class="text-white/80 text-sm font-bold">🤝</div>
|
||||||
|
<!-- Dutch Flag -->
|
||||||
|
<div class="flex flex-col shadow-lg rounded-lg overflow-hidden border-2 border-white/20">
|
||||||
|
<div class="w-18 h-2 bg-dutch-red-500"></div>
|
||||||
|
<div class="w-18 h-2 bg-white"></div>
|
||||||
|
<div class="w-18 h-2 bg-dutch-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Decorative Flag Elements -->
|
||||||
|
<div class="absolute top-8 right-8 flex items-center gap-2 z-20">
|
||||||
|
<div class="text-white/60 text-xs uppercase tracking-wider font-semibold">Nigeria 🇳🇬 • Netherlands 🇳🇱</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Subtle Pattern Overlay -->
|
||||||
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
|
|
||||||
|
<!-- Main Content -->
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<!-- Breadcrumb with Flag Accents -->
|
||||||
|
<div class="flex items-center justify-center gap-2 mb-6 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>About</span>
|
<span>About</span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
|
||||||
About Us
|
<!-- Title with Flag Integration -->
|
||||||
</h1>
|
<div class="mb-6">
|
||||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Discover the rich heritage and noble mission of the Omoluabi Association in the Netherlands.
|
About Us
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<!-- Flag Strip Under Title -->
|
||||||
|
<div class="flex items-center justify-center gap-1 mb-4">
|
||||||
|
<div class="flex rounded-sm overflow-hidden shadow-md">
|
||||||
|
<div class="h-1 w-12 bg-nigerian-green-500"></div>
|
||||||
|
<div class="h-1 w-12 bg-white"></div>
|
||||||
|
<div class="h-1 w-12 bg-nigerian-green-500"></div>
|
||||||
|
</div>
|
||||||
|
<div class="w-4 h-px bg-gradient-to-r from-white/60 to-transparent"></div>
|
||||||
|
<div class="text-white/60 text-xs">🤝</div>
|
||||||
|
<div class="w-4 h-px bg-gradient-to-l from-white/60 to-transparent"></div>
|
||||||
|
<div class="flex flex-col rounded-sm overflow-hidden shadow-md">
|
||||||
|
<div class="w-12 h-px bg-dutch-red-500"></div>
|
||||||
|
<div class="w-12 h-px bg-white"></div>
|
||||||
|
<div class="w-12 h-px bg-dutch-blue-500"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto leading-relaxed">
|
||||||
|
Discover the rich heritage and noble mission of the Omoluabi Association bridging
|
||||||
|
<span class="font-semibold text-nigerian-green-200">Nigerian culture</span> with
|
||||||
|
<span class="font-semibold text-dutch-blue-200">Dutch hospitality</span> in the Netherlands.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<!-- Cultural Bridge Visual -->
|
||||||
|
<div class="mt-8 flex items-center justify-center gap-6">
|
||||||
|
<div class="flex items-center gap-2 bg-white/10 px-4 py-2 rounded-full backdrop-blur-sm border border-white/20">
|
||||||
|
<div class="w-6 h-4 bg-nigerian-green-500 rounded-sm shadow-sm"></div>
|
||||||
|
<span class="text-sm font-medium">Nigerian Heritage</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-2xl animate-pulse">🌉</div>
|
||||||
|
<div class="flex items-center gap-2 bg-white/10 px-4 py-2 rounded-full backdrop-blur-sm border border-white/20">
|
||||||
|
<div class="w-6 h-4 bg-dutch-red-500 rounded-sm shadow-sm"></div>
|
||||||
|
<span class="text-sm font-medium">Dutch Integration</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -283,7 +355,7 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- CTA Section -->
|
<!-- CTA Section -->
|
||||||
<section class="section bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500 text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
|
<section class="section combined-flag-gradient text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
|
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
|
||||||
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
||||||
|
|
||||||
|
@@ -5,13 +5,14 @@ import ContactForm from '../components/ContactForm.jsx';
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>Contact</span>
|
<span>Contact</span>
|
||||||
|
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Contact Us
|
Contact Us
|
||||||
|
@@ -5,13 +5,14 @@ import DonationProgressBar from '../components/DonationProgressBar.jsx';
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>Donate</span>
|
<span>Donate</span>
|
||||||
|
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Support Our Mission
|
Support Our Mission
|
||||||
|
@@ -9,13 +9,14 @@ const sortedEvents = events.sort((a, b) => new Date(b.frontmatter?.date || 0) -
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>Events</span>
|
<span>Events</span>
|
||||||
|
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Our Events
|
Our Events
|
||||||
|
@@ -173,7 +173,7 @@ import HomeGallery from '../components/HomeGallery.astro';
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<!-- Donation Section -->
|
<!-- Donation Section -->
|
||||||
<section class="section bg-gradient-to-br from-ankara-red-500 via-kente-gold-500 to-nigerian-green-500 text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
|
<section class="section combined-flag-gradient text-white text-center relative overflow-hidden" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
|
<div class="absolute top-10 left-10 w-32 h-32 bg-white/10 rounded-full animate-float"></div>
|
||||||
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
<div class="absolute bottom-10 right-10 w-24 h-24 bg-white/10 rounded-full animate-float animation-delay-1s"></div>
|
||||||
|
|
||||||
|
@@ -7,13 +7,14 @@ const members = memberFiles[0]?.frontmatter?.members || [];
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width=\"60\" height=\"60\" viewBox=\"0 0 60 60\" xmlns=\"http://www.w3.org/2000/svg\"%3E%3Cg fill=\"none\" fill-rule=\"evenodd\"%3E%3Cg fill=\"%23ffffff\" fill-opacity=\"0.1\"%3E%3Cpath d=\"M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z\"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>Members</span>
|
<span>Members</span>
|
||||||
|
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Meet Our Members
|
Meet Our Members
|
||||||
|
@@ -4,13 +4,14 @@ import BaseLayout from '../layouts/BaseLayout.astro';
|
|||||||
|
|
||||||
<BaseLayout>
|
<BaseLayout>
|
||||||
<!-- Page Header -->
|
<!-- Page Header -->
|
||||||
<section class="relative py-16 px-4 text-center text-white overflow-hidden bg-gradient-to-br from-nigerian-green-500 via-kente-gold-500 to-ankara-red-500" data-animate-on-scroll="fade-in">
|
<section class="relative py-16 px-4 text-center text-white overflow-hidden nigerian-flag-bg" data-animate-on-scroll="fade-in">
|
||||||
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
<div class="absolute inset-0 opacity-10" style="background-image: url('data:image/svg+xml,%3Csvg width="60" height="60" viewBox="0 0 60 60" xmlns="http://www.w3.org/2000/svg"%3E%3Cg fill="none" fill-rule="evenodd"%3E%3Cg fill="%23ffffff" fill-opacity="0.1"%3E%3Cpath d="M36 34v-4h-2v4h-4v2h4v4h2v-4h4v-2h-4zm0-30V0h-2v4h-4v2h4v4h2V6h4V4h-4zM6 34v-4H4v4H0v2h4v4h2v-4h4v-2H6zM6 4V0H4v4H0v2h4v4h2V6h4V4H6z"/%3E%3C/g%3E%3C/g%3E%3C/svg%3E');"></div>
|
||||||
<div class="relative z-10 max-w-4xl mx-auto">
|
<div class="relative z-10 max-w-4xl mx-auto">
|
||||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
<div class="flex items-center justify-center gap-2 mb-4 text-sm">
|
||||||
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
<a href="/" class="text-white/80 hover:text-white transition-colors">🏠 Home</a>
|
||||||
<span class="text-white/60">•</span>
|
<span class="text-white/60">•</span>
|
||||||
<span>Orphanage</span>
|
<span>Orphanage</span>
|
||||||
|
<span class="ml-4"><span class="dutch-flag-badge"><span class="stripe red"></span><span class="stripe white"></span><span class="stripe blue"></span></span></span>
|
||||||
</div>
|
</div>
|
||||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight bg-clip-text text-transparent bg-gradient-to-r from-white to-gray-200 animate-text-shine">
|
||||||
Our Orphanage Program
|
Our Orphanage Program
|
||||||
|
@@ -11,6 +11,10 @@
|
|||||||
--kente-gold: #f59e0b;
|
--kente-gold: #f59e0b;
|
||||||
--ankara-red: #dc2626;
|
--ankara-red: #dc2626;
|
||||||
--adire-blue: #2563eb;
|
--adire-blue: #2563eb;
|
||||||
|
/* Dutch Flag Colors */
|
||||||
|
--dutch-red: #21468b;
|
||||||
|
--dutch-white: #ffffff;
|
||||||
|
--dutch-blue: #1e4785;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar */
|
/* Custom scrollbar */
|
||||||
|
@@ -211,11 +211,29 @@
|
|||||||
box-shadow: 0 0 20px rgba(22, 163, 74, 0.6);
|
box-shadow: 0 0 20px rgba(22, 163, 74, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nigerian Flag Colors */
|
/* Nigerian Flag Background (vertical stripes) */
|
||||||
.nigerian-gradient {
|
.nigerian-flag-bg {
|
||||||
background: linear-gradient(90deg, #16a34a 0%, #ffffff 50%, #16a34a 100%);
|
background: linear-gradient(90deg, var(--nigerian-green) 0%, var(--nigerian-green) 33.33%, #fff 33.33%, #fff 66.66%, var(--nigerian-green) 66.66%, var(--nigerian-green) 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Dutch Flag Badge (horizontal stripes) */
|
||||||
|
.dutch-flag-badge {
|
||||||
|
width: 48px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
overflow: hidden;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 1px 4px rgba(0,0,0,0.08);
|
||||||
|
border: 1px solid #eee;
|
||||||
|
}
|
||||||
|
.dutch-flag-badge .stripe {
|
||||||
|
height: 33.33%;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.dutch-flag-badge .red { background: var(--dutch-red); }
|
||||||
|
.dutch-flag-badge .white { background: #fff; }
|
||||||
|
.dutch-flag-badge .blue { background: var(--dutch-blue); }
|
||||||
|
|
||||||
/* Kente Pattern Inspired Background */
|
/* Kente Pattern Inspired Background */
|
||||||
.kente-pattern {
|
.kente-pattern {
|
||||||
background-image:
|
background-image:
|
||||||
@@ -341,4 +359,43 @@
|
|||||||
.text-gray-400 {
|
.text-gray-400 {
|
||||||
color: #9ca3af;
|
color: #9ca3af;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Nigerian + Dutch Flag Combined Gradient */
|
||||||
|
.combined-flag-gradient {
|
||||||
|
background: linear-gradient(90deg, var(--nigerian-green) 0%, var(--dutch-white) 20%, var(--dutch-red) 40%, var(--dutch-white) 60%, var(--dutch-blue) 80%, var(--nigerian-green) 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Combined Flag Button */
|
||||||
|
.btn-combined-flag {
|
||||||
|
background: linear-gradient(135deg, var(--nigerian-green) 0%, var(--dutch-red) 33%, var(--dutch-white) 66%, var(--dutch-blue) 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.75rem 1.5rem;
|
||||||
|
border-radius: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-combined-flag::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: -100%;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: linear-gradient(135deg, var(--dutch-blue) 0%, var(--dutch-white) 50%, var(--nigerian-green) 100%);
|
||||||
|
transition: left 0.3s ease;
|
||||||
|
z-index: -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-combined-flag:hover::before {
|
||||||
|
left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-combined-flag:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.2);
|
||||||
}
|
}
|
Reference in New Issue
Block a user