Compare commits
13 Commits
d2bb939dc8
...
main
Author | SHA1 | Date | |
---|---|---|---|
99f92eaa7b | |||
0f86f8f615 | |||
725a3e8e3a | |||
ba9bb6dfe0 | |||
c8c550a00b | |||
a0ab66189a | |||
3c74d71e22 | |||
910253c8f4 | |||
45fc361eb4 | |||
091324ca51 | |||
31e72ecfce | |||
1656b95a33 | |||
9f6fa46227 |
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.
|
@@ -1,15 +1,13 @@
|
||||
// @ts-check
|
||||
// astro.config.mjs
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
import tailwind from '@astrojs/tailwind';
|
||||
import react from '@astrojs/react';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
vite: {
|
||||
plugins: [tailwindcss()]
|
||||
},
|
||||
|
||||
integrations: [react()]
|
||||
integrations: [
|
||||
react(),
|
||||
tailwind({
|
||||
applyBaseStyles: false, // Prevents conflicts with custom styles
|
||||
})
|
||||
],
|
||||
});
|
1350
package-lock.json
generated
1350
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -13,11 +13,13 @@
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"astro": "^5.9.4",
|
||||
"daisyui": "^5.0.43",
|
||||
"astro": "^5.12.9",
|
||||
"keen-slider": "^6.8.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"tailwindcss": "^4.1.10"
|
||||
"tailwindcss": "^3.4.17"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@astrojs/tailwind": "^6.0.2"
|
||||
}
|
||||
}
|
||||
|
@@ -1,46 +1,147 @@
|
||||
import { useState } from "react";
|
||||
import React, { useState } from 'react';
|
||||
|
||||
export default function ContactForm() {
|
||||
const [status, setStatus] = useState("");
|
||||
const [formData, setFormData] = useState({
|
||||
name: '',
|
||||
email: '',
|
||||
subject: '',
|
||||
message: '',
|
||||
});
|
||||
|
||||
const [errors, setErrors] = useState({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [submitMessage, setSubmitMessage] = useState(null);
|
||||
const [hp, setHp] = useState('');
|
||||
|
||||
const validate = () => {
|
||||
let newErrors = {};
|
||||
if (!formData.name) newErrors.name = 'Name is required';
|
||||
if (!formData.email) {
|
||||
newErrors.email = 'Email is required';
|
||||
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,4}$/i.test(formData.email)) {
|
||||
newErrors.email = 'Invalid email address';
|
||||
}
|
||||
if (!formData.subject) newErrors.subject = 'Subject is required';
|
||||
if (!formData.message) newErrors.message = 'Message is required';
|
||||
return newErrors;
|
||||
};
|
||||
|
||||
const handleChange = (e) => {
|
||||
const { name, value } = e.target;
|
||||
setFormData({ ...formData, [name]: value });
|
||||
// Clear error for the field as user types
|
||||
if (errors[name]) {
|
||||
setErrors({ ...errors, [name]: null });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
const form = e.target;
|
||||
// Honeypot check
|
||||
if (form.honey.value !== "") {
|
||||
setStatus("Spam detected.");
|
||||
return;
|
||||
setIsSubmitting(true);
|
||||
setSubmitMessage(null);
|
||||
|
||||
const validationErrors = validate();
|
||||
setErrors(validationErrors);
|
||||
|
||||
if (Object.keys(validationErrors).length === 0) {
|
||||
try {
|
||||
// Simulate API call
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
console.log('Form data submitted:', formData);
|
||||
setSubmitMessage({ type: 'success', text: 'Your message has been sent successfully!' });
|
||||
setFormData({ name: '', email: '', subject: '', message: '' }); // Clear form
|
||||
} catch (error) {
|
||||
setSubmitMessage({ type: 'error', text: 'There was an error sending your message. Please try again later.' });
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
} else {
|
||||
setIsSubmitting(false);
|
||||
setSubmitMessage({ type: 'error', text: 'Please correct the errors in the form.' });
|
||||
}
|
||||
setStatus("Sending...");
|
||||
// Placeholder: would POST to /api/contact
|
||||
setTimeout(() => setStatus("Message sent (placeholder, not yet implemented)."), 1000);
|
||||
};
|
||||
|
||||
return (
|
||||
<form className="max-w-xl mx-auto bg-base-100 p-8 rounded-xl shadow-lg" onSubmit={handleSubmit} autoComplete="off">
|
||||
<div className="mb-4">
|
||||
<label htmlFor="name" className="block font-bold mb-1">Name</label>
|
||||
<input type="text" id="name" name="name" className="input input-bordered w-full" required autoComplete="off" />
|
||||
<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>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="email" className="block font-bold mb-1">Email</label>
|
||||
<input type="email" id="email" name="email" className="input input-bordered w-full" required autoComplete="off" />
|
||||
<h2 className="text-3xl font-bold text-center text-nigerian-green-700 mb-8">Send Us a Message</h2>
|
||||
|
||||
{submitMessage && (
|
||||
<div className={`p-4 mb-4 rounded-lg text-center ${submitMessage.type === 'success' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
|
||||
{submitMessage.text}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="mb-5">
|
||||
<label htmlFor="name" className="block text-gray-700 text-sm font-bold mb-2">Name</label>
|
||||
<input
|
||||
type="text"
|
||||
id="name"
|
||||
name="name"
|
||||
value={formData.name}
|
||||
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'}`}
|
||||
placeholder="Your Name"
|
||||
/>
|
||||
{errors.name && <p id="name-error" className="text-red-500 text-xs italic mt-2">{errors.name}</p>}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="subject" className="block font-bold mb-1">Subject</label>
|
||||
<input type="text" id="subject" name="subject" className="input input-bordered w-full" required autoComplete="off" />
|
||||
|
||||
<div className="mb-5">
|
||||
<label htmlFor="email" className="block text-gray-700 text-sm font-bold mb-2">Email</label>
|
||||
<input
|
||||
type="email"
|
||||
id="email"
|
||||
name="email"
|
||||
value={formData.email}
|
||||
onChange={handleChange}
|
||||
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.email ? 'border-red-500' : 'border-gray-300'}`}
|
||||
placeholder="your.email@example.com"
|
||||
/>
|
||||
{errors.email && <p className="text-red-500 text-xs italic mt-2">{errors.email}</p>}
|
||||
</div>
|
||||
<div className="mb-4">
|
||||
<label htmlFor="message" className="block font-bold mb-1">Message</label>
|
||||
<textarea id="message" name="message" className="textarea textarea-bordered w-full" rows={5} required></textarea>
|
||||
|
||||
<div className="mb-5">
|
||||
<label htmlFor="subject" className="block text-gray-700 text-sm font-bold mb-2">Subject</label>
|
||||
<input
|
||||
type="text"
|
||||
id="subject"
|
||||
name="subject"
|
||||
value={formData.subject}
|
||||
onChange={handleChange}
|
||||
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.subject ? 'border-red-500' : 'border-gray-300'}`}
|
||||
placeholder="Subject of your message"
|
||||
/>
|
||||
{errors.subject && <p className="text-red-500 text-xs italic mt-2">{errors.subject}</p>}
|
||||
</div>
|
||||
{/* Honeypot field for spam protection */}
|
||||
<div style={{ display: "none" }}>
|
||||
<label htmlFor="honey">Do not fill this out</label>
|
||||
<input type="text" id="honey" name="honey" tabIndex="-1" autoComplete="off" />
|
||||
|
||||
<div className="mb-6">
|
||||
<label htmlFor="message" className="block text-gray-700 text-sm font-bold mb-2">Message</label>
|
||||
<textarea
|
||||
id="message"
|
||||
name="message"
|
||||
rows="6"
|
||||
value={formData.message}
|
||||
onChange={handleChange}
|
||||
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.message ? 'border-red-500' : 'border-gray-300'}`}
|
||||
placeholder="Your message..."
|
||||
></textarea>
|
||||
{errors.message && <p className="text-red-500 text-xs italic mt-2">{errors.message}</p>}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-center">
|
||||
<button
|
||||
type="submit"
|
||||
className="bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 hover:from-nigerian-green-600 hover:to-kente-gold-600 text-white font-bold py-3 px-8 rounded-lg focus:outline-none focus:shadow-outline transition-all duration-300 transform hover:scale-105"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
{isSubmitting ? 'Sending...' : 'Send Message'}
|
||||
</button>
|
||||
</div>
|
||||
<button type="submit" className="btn btn-primary w-full">Send Message</button>
|
||||
{status && <div className="mt-4 text-center text-sm text-success">{status}</div>}
|
||||
</form>
|
||||
);
|
||||
}
|
||||
}
|
85
src/components/DonationProgressBar.jsx
Normal file
85
src/components/DonationProgressBar.jsx
Normal file
@@ -0,0 +1,85 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
const MONTHLY_GOAL = 5000; // €5000 monthly goal
|
||||
|
||||
export default function DonationProgressBar() {
|
||||
const [currentAmount, setCurrentAmount] = useState(0);
|
||||
const [lastResetDate, setLastResetDate] = useState(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Load data from localStorage
|
||||
const storedAmount = localStorage.getItem('omoluabi_donation_amount');
|
||||
const storedResetDate = localStorage.getItem('omoluabi_donation_last_reset');
|
||||
|
||||
let amount = storedAmount ? parseFloat(storedAmount) : 0;
|
||||
let resetDate = storedResetDate ? new Date(storedResetDate) : null;
|
||||
|
||||
const now = new Date();
|
||||
const firstDayOfCurrentMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
|
||||
// Check if it's a new month or if resetDate is not set
|
||||
if (!resetDate || resetDate.getMonth() !== now.getMonth() || resetDate.getFullYear() !== now.getFullYear()) {
|
||||
// Reset for the new month
|
||||
amount = 0;
|
||||
resetDate = firstDayOfCurrentMonth;
|
||||
localStorage.setItem('omoluabi_donation_amount', amount.toString());
|
||||
localStorage.setItem('omoluabi_donation_last_reset', resetDate.toISOString());
|
||||
}
|
||||
|
||||
setCurrentAmount(amount);
|
||||
setLastResetDate(resetDate);
|
||||
}, []);
|
||||
|
||||
const percentage = Math.min(100, (currentAmount / MONTHLY_GOAL) * 100);
|
||||
|
||||
// This function is for demonstration purposes. In a real app, donations would come from a payment gateway.
|
||||
const handleDonate = (amount) => {
|
||||
setCurrentAmount(prevAmount => {
|
||||
const newAmount = prevAmount + amount;
|
||||
localStorage.setItem('omoluabi_donation_amount', newAmount.toString());
|
||||
return newAmount;
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white p-6 rounded-xl shadow-lg border border-gray-200">
|
||||
<h2 className="text-2xl font-bold text-center mb-4 text-nigerian-green-700">Monthly Donation Goal</h2>
|
||||
<p className="text-center text-gray-600 mb-6">Help us reach our goal to support our initiatives!</p>
|
||||
|
||||
<div className="w-full bg-gray-200 rounded-full h-4 mb-4 overflow-hidden">
|
||||
<div
|
||||
className="h-4 rounded-full bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 transition-all duration-1000 ease-out"
|
||||
style={{ width: `${percentage}%` }}
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center mb-6">
|
||||
<span className="text-lg font-semibold text-gray-800">€{currentAmount.toLocaleString()}</span>
|
||||
<span className="text-lg font-semibold text-gray-800">€{MONTHLY_GOAL.toLocaleString()}</span>
|
||||
</div>
|
||||
|
||||
<div className="text-center text-sm text-gray-500 mb-6">
|
||||
{percentage.toFixed(1)}% of the goal reached this month.
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-6">
|
||||
{[10, 25, 50, 100, 250, 500].map(amount => (
|
||||
<button
|
||||
key={amount}
|
||||
onClick={() => handleDonate(amount)}
|
||||
className="bg-nigerian-green-100 text-nigerian-green-700 font-semibold py-2 px-4 rounded-lg hover:bg-nigerian-green-200 transition-colors duration-200 shadow-sm"
|
||||
>
|
||||
Donate €{amount}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => handleDonate(100)} // Example: default donation
|
||||
className="w-full bg-gradient-to-r from-ankara-red-500 to-kente-gold-500 text-white font-bold py-3 rounded-lg shadow-md hover:shadow-lg transform hover:scale-105 transition-all duration-300"
|
||||
>
|
||||
Make a Custom Donation
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
82
src/components/EventFilterSearch.jsx
Normal file
82
src/components/EventFilterSearch.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
|
||||
export default function EventFilterSearch({ events }) {
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
const [selectedCategory, setSelectedCategory] = useState('All');
|
||||
const [filteredEvents, setFilteredEvents] = useState(events);
|
||||
|
||||
const categories = ['All', ...new Set(events.map(event => event.frontmatter.category))];
|
||||
|
||||
useEffect(() => {
|
||||
let tempEvents = events;
|
||||
|
||||
// Filter by category
|
||||
if (selectedCategory !== 'All') {
|
||||
tempEvents = tempEvents.filter(event => event.frontmatter.category === selectedCategory);
|
||||
}
|
||||
|
||||
// Filter by search term
|
||||
if (searchTerm) {
|
||||
tempEvents = tempEvents.filter(event =>
|
||||
event.frontmatter.title.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
event.frontmatter.description.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
);
|
||||
}
|
||||
|
||||
setFilteredEvents(tempEvents);
|
||||
}, [searchTerm, selectedCategory, events]);
|
||||
|
||||
return (
|
||||
<div className="container mx-auto px-4 py-8">
|
||||
<div className="flex flex-col md:flex-row gap-4 mb-8">
|
||||
{/* Search Input */}
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search events..."
|
||||
className="flex-grow p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-nigerian-green-500"
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
/>
|
||||
|
||||
{/* Category Filter */}
|
||||
<select
|
||||
className="p-3 border border-gray-300 rounded-lg shadow-sm focus:outline-none focus:ring-2 focus:ring-nigerian-green-500"
|
||||
value={selectedCategory}
|
||||
onChange={(e) => setSelectedCategory(e.target.value)}
|
||||
>
|
||||
{categories.map(category => (
|
||||
<option key={category} value={category}>{category}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Event List */}
|
||||
<section className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-8">
|
||||
{filteredEvents.length > 0 ? (
|
||||
filteredEvents.map(event => (
|
||||
<article key={event.url} className="card bg-base-100 shadow-lg rounded-xl overflow-hidden transform transition-transform duration-300 hover:scale-105 hover:shadow-xl">
|
||||
<figure className="relative h-48 w-full overflow-hidden">
|
||||
<img src={event.frontmatter.image} alt={event.frontmatter.title} className="w-full h-full object-cover" />
|
||||
<div className="absolute inset-0 bg-gradient-to-t from-black/60 to-transparent"></div>
|
||||
<span className="absolute bottom-3 left-3 badge badge-secondary bg-kente-gold-500 text-white px-3 py-1 rounded-full text-sm font-semibold">{event.frontmatter.category}</span>
|
||||
</figure>
|
||||
<div className="card-body p-6">
|
||||
<h2 className="card-title text-xl font-bold text-nigerian-green-700 mb-2">{event.frontmatter.title}</h2>
|
||||
<p className="text-sm text-gray-600 mb-3">{new Date(event.frontmatter.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' })}</p>
|
||||
<p className="text-gray-700 leading-relaxed text-sm mb-4">{event.frontmatter.description}</p>
|
||||
<a href={event.url} className="inline-flex items-center text-nigerian-green-600 hover:text-nigerian-green-800 font-semibold transition-colors duration-200">
|
||||
Read More
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4 ml-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
) : (
|
||||
<p className="text-center text-gray-500 text-lg col-span-full">No events found matching your criteria.</p>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -95,6 +95,9 @@
|
||||
<a href="/orphanage" class="block text-gray-300 hover:text-nigerian-green-400 transition-colors duration-200 hover:translate-x-1 transform">
|
||||
Orphanage
|
||||
</a>
|
||||
<a href="/gallery" class="block text-gray-300 hover:text-nigerian-green-400 transition-colors duration-200 hover:translate-x-1 transform">
|
||||
Gallery
|
||||
</a>
|
||||
<a href="/contact" class="block text-gray-300 hover:text-nigerian-green-400 transition-colors duration-200 hover:translate-x-1 transform">
|
||||
Contact
|
||||
</a>
|
||||
|
@@ -73,6 +73,10 @@
|
||||
Orphanage
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-green-600 group-hover:w-full transition-all duration-300"></span>
|
||||
</a>
|
||||
<a href="/gallery" class="nav-link px-4 py-2 rounded-lg font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-all duration-200 relative group">
|
||||
Gallery
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-green-600 group-hover:w-full transition-all duration-300"></span>
|
||||
</a>
|
||||
<a href="/contact" class="nav-link px-4 py-2 rounded-lg font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-all duration-200 relative group">
|
||||
Contact
|
||||
<span class="absolute bottom-0 left-0 w-0 h-0.5 bg-green-600 group-hover:w-full transition-all duration-300"></span>
|
||||
@@ -122,6 +126,9 @@
|
||||
<a href="/orphanage" class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-all duration-200">
|
||||
🏠 Orphanage
|
||||
</a>
|
||||
<a href="/gallery" class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-all duration-200">
|
||||
🖼️ Gallery
|
||||
</a>
|
||||
<a href="/contact" class="block px-4 py-3 rounded-lg font-medium text-gray-700 hover:text-green-600 hover:bg-green-50 transition-all duration-200">
|
||||
📞 Contact
|
||||
</a>
|
||||
|
@@ -78,7 +78,8 @@ export default function HeroCarousel() {
|
||||
};
|
||||
|
||||
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 */}
|
||||
<div className="relative h-[600px] overflow-hidden">
|
||||
{slides.map((slide, index) => {
|
||||
@@ -93,6 +94,9 @@ export default function HeroCarousel() {
|
||||
? 'opacity-0 -translate-x-full'
|
||||
: 'opacity-0 translate-x-full'
|
||||
}`}
|
||||
role="group"
|
||||
aria-roledescription="slide"
|
||||
aria-label={`${index + 1} of ${slides.length}`}
|
||||
>
|
||||
{/* Background Image */}
|
||||
<div className="absolute inset-0">
|
||||
@@ -132,10 +136,10 @@ export default function HeroCarousel() {
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col sm:flex-row gap-4">
|
||||
<button className="bg-white text-gray-900 px-8 py-4 rounded-xl font-bold hover:bg-gray-100 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||
<button className="bg-white dark:bg-gray-900 dark:text-white text-gray-900 px-8 py-4 rounded-xl font-bold hover:bg-gray-100 dark:hover:bg-gray-800 transition-all duration-300 transform hover:scale-105 shadow-lg">
|
||||
{slide.cta}
|
||||
</button>
|
||||
<button className="border-2 border-white text-white px-8 py-4 rounded-xl font-bold hover:bg-white hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||
<button className="border-2 border-white text-white px-8 py-4 rounded-xl font-bold hover:bg-white hover:text-gray-900 dark:hover:text-gray-900 transition-all duration-300 transform hover:scale-105">
|
||||
Learn More
|
||||
</button>
|
||||
</div>
|
||||
@@ -194,6 +198,7 @@ export default function HeroCarousel() {
|
||||
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"
|
||||
aria-label={isPlaying ? "Pause slideshow" : "Play slideshow"}
|
||||
aria-pressed={!isPlaying}
|
||||
>
|
||||
{isPlaying ? (
|
||||
<svg className="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
|
||||
|
@@ -2,13 +2,24 @@
|
||||
const gallery = await Astro.glob('../../content/gallery/gallery-sample.md');
|
||||
const images = gallery[0]?.frontmatter.images || [];
|
||||
---
|
||||
<section class="max-w-6xl mx-auto my-20">
|
||||
<h2 class="text-2xl font-headline font-bold mb-6 text-primary text-center">Gallery</h2>
|
||||
<section class="max-w-6xl mx-auto my-20 px-4">
|
||||
<h2 class="text-3xl font-headline font-bold mb-6 text-nigerian-green-700 dark:text-kente-gold-400 text-center">Our Photo Gallery</h2>
|
||||
<p class="text-center text-lg text-gray-600 dark:text-gray-300 mb-10">
|
||||
A glimpse into our vibrant community and cultural celebrations.
|
||||
</p>
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{images.map((img, idx) => (
|
||||
<div class="rounded-lg overflow-hidden shadow-md bg-base-100" key={idx}>
|
||||
{images.slice(0, 4).map((img: string, idx: number) => (
|
||||
<div class="rounded-lg overflow-hidden shadow-md bg-white dark:bg-gray-800">
|
||||
<img src={img} alt={`Gallery photo ${idx + 1}`} class="w-full h-40 object-cover hover:scale-105 transition-transform duration-200 cursor-pointer" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
<div class="text-center mt-10">
|
||||
<a href="/gallery" class="inline-flex items-center px-6 py-3 bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 text-white rounded-lg shadow-lg hover:shadow-xl hover:from-nigerian-green-600 hover:to-kente-gold-600 transition-all duration-200 font-medium">
|
||||
View Full Gallery
|
||||
<svg class="w-5 h-5 ml-2" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fill-rule="evenodd" d="M4 3a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V5a2 2 0 00-2-2H4zm12 12H4l4-8 3 6 2-4 3 6z" clip-rule="evenodd"></path>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
</section>
|
58
src/components/ImageGallery.jsx
Normal file
58
src/components/ImageGallery.jsx
Normal file
@@ -0,0 +1,58 @@
|
||||
import React, { useState } from 'react';
|
||||
import Lightbox from './Lightbox.jsx'; // Assuming Lightbox component will be created
|
||||
|
||||
export default function ImageGallery({ images }) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [currentImageIndex, setCurrentImageIndex] = useState(0);
|
||||
|
||||
const openLightbox = (index) => {
|
||||
setCurrentImageIndex(index);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const closeLightbox = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const goToNext = () => {
|
||||
setCurrentImageIndex((prevIndex) => (prevIndex + 1) % images.length);
|
||||
};
|
||||
|
||||
const goToPrev = () => {
|
||||
setCurrentImageIndex((prevIndex) => (prevIndex - 1 + images.length) % images.length);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{images.map((image, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="relative overflow-hidden rounded-lg shadow-md cursor-pointer group"
|
||||
onClick={() => openLightbox(index)}
|
||||
>
|
||||
<img
|
||||
src={image}
|
||||
alt={`Gallery image ${index + 1}`}
|
||||
className="w-full h-48 object-cover transform transition-transform duration-300 group-hover:scale-110"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-black bg-opacity-40 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300">
|
||||
<svg className="w-10 h-10 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"></path>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{isOpen && (
|
||||
<Lightbox
|
||||
images={images}
|
||||
currentIndex={currentImageIndex}
|
||||
onClose={closeLightbox}
|
||||
onNext={goToNext}
|
||||
onPrev={goToPrev}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
69
src/components/Lightbox.jsx
Normal file
69
src/components/Lightbox.jsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import React, { useEffect } from 'react';
|
||||
|
||||
export default function Lightbox({ images, currentIndex, onClose, onNext, onPrev }) {
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
} else if (e.key === 'ArrowRight') {
|
||||
onNext();
|
||||
} else if (e.key === 'ArrowLeft') {
|
||||
onPrev();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleKeyDown);
|
||||
};
|
||||
}, [onClose, onNext, onPrev]);
|
||||
|
||||
if (!images || images.length === 0) return null;
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
<img
|
||||
src={images[currentIndex]}
|
||||
alt={`Gallery image ${currentIndex + 1}`}
|
||||
className="max-w-full max-h-[80vh] object-contain rounded-lg shadow-xl"
|
||||
/>
|
||||
|
||||
{/* Close Button */}
|
||||
<button
|
||||
className="absolute top-4 right-4 text-white text-3xl p-2 rounded-full bg-white/20 hover:bg-white/40 transition-colors duration-200"
|
||||
onClick={onClose}
|
||||
aria-label="Close Lightbox"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
|
||||
{/* Navigation Buttons */}
|
||||
{images.length > 1 && (
|
||||
<>
|
||||
<button
|
||||
className="absolute left-4 top-1/2 -translate-y-1/2 text-white text-4xl p-2 rounded-full bg-white/20 hover:bg-white/40 transition-colors duration-200"
|
||||
onClick={onPrev}
|
||||
aria-label="Previous Image"
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<button
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-white text-4xl p-2 rounded-full bg-white/20 hover:bg-white/40 transition-colors duration-200"
|
||||
onClick={onNext}
|
||||
aria-label="Next Image"
|
||||
>
|
||||
›
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Image Counter */}
|
||||
<div className="absolute bottom-4 left-1/2 -translate-x-1/2 text-white text-lg bg-black/50 px-4 py-2 rounded-full">
|
||||
{currentIndex + 1} / {images.length}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
42
src/components/ThemeToggle.jsx
Normal file
42
src/components/ThemeToggle.jsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export default function ThemeToggle() {
|
||||
const [isDark, setIsDark] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const stored = localStorage.getItem('omoluabi_theme');
|
||||
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
const initial = stored ? stored === 'dark' : prefersDark;
|
||||
setIsDark(initial);
|
||||
document.documentElement.classList.toggle('dark', initial);
|
||||
}, []);
|
||||
|
||||
const toggle = () => {
|
||||
const next = !isDark;
|
||||
setIsDark(next);
|
||||
document.documentElement.classList.toggle('dark', next);
|
||||
localStorage.setItem('omoluabi_theme', next ? 'dark' : 'light');
|
||||
};
|
||||
|
||||
return (
|
||||
<button
|
||||
onClick={toggle}
|
||||
className="inline-flex items-center gap-2 px-3 py-2 rounded-lg border border-gray-200 dark:border-gray-700 bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
aria-label="Toggle color theme"
|
||||
>
|
||||
{isDark ? (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"><path d="M21.64 13a1 1 0 0 0-1.05-.14 8 8 0 1 1-9.45-9.45 1 1 0 0 0-.14-2A10 10 0 1 0 23 14a1 1 0 0 0-1.36-1z"/></svg>
|
||||
<span className="text-sm">Dark</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" className="h-4 w-4" fill="currentColor" viewBox="0 0 24 24"><path d="M6.76 4.84l-1.8-1.79-1.41 1.41 1.79 1.8 1.42-1.42zm10.45 14.32l1.79 1.8 1.41-1.41-1.8-1.79-1.4 1.4zM12 4a1 1 0 0 1 1 1v2a1 1 0 1 1-2 0V5a1 1 0 0 1 1-1zm0 12a4 4 0 1 0 0-8 4 4 0 0 0 0 8zm8-5h-2a1 1 0 1 1 0-2h2a1 1 0 1 1 0 2zM4 13H2a1 1 0 1 1 0-2h2a1 1 0 1 1 0 2zm2.76 6.36l-1.42 1.42-1.79-1.8 1.41-1.41 1.8 1.79zM19.78 4.46l-1.41-1.41-1.8 1.79 1.41 1.41 1.8-1.79z"/></svg>
|
||||
<span className="text-sm">Light</span>
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -1,41 +1,170 @@
|
||||
---
|
||||
// Base layout for all pages
|
||||
import Header from '../components/Header.astro';
|
||||
import Footer from '../components/Footer.astro';
|
||||
// src/layouts/BaseLayout.astro
|
||||
import '../styles/global.css';
|
||||
import '../styles/main.css';
|
||||
import ThemeToggle from '../components/ThemeToggle.jsx';
|
||||
|
||||
export interface Props {
|
||||
title?: string;
|
||||
description?: string;
|
||||
lang?: string;
|
||||
}
|
||||
|
||||
const { title = "Omoluabi Association Netherlands", description = "Preserving Nigerian culture and heritage in the Netherlands" } = Astro.props;
|
||||
---
|
||||
<html lang="en">
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang={Astro.props.lang ?? 'en'}>
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Omoluabi Foundation</title>
|
||||
<meta name="description" content="Supporting Nigerians in the Netherlands" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
||||
<slot name="head" />
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<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>
|
||||
<body style="
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f0fdf4 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-top: 80px;
|
||||
">
|
||||
<Header />
|
||||
<main style="flex: 1;">
|
||||
|
||||
<body class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100">
|
||||
<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 -->
|
||||
<header class="bg-white dark:bg-gray-900/95 backdrop-blur shadow-md sticky top-0 z-50 border-b border-gray-200 dark:border-white/10">
|
||||
<nav class="container mx-auto px-4 py-4">
|
||||
<div class="flex items-center justify-between">
|
||||
<!-- Logo -->
|
||||
<div class="flex items-center space-x-2">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-nigerian-green-500 to-kente-gold-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-bold text-lg">O</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-headline font-bold text-xl text-nigerian-green-700">Omoluabi</div>
|
||||
<div class="text-sm text-gray-600">Foundation</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Desktop Navigation -->
|
||||
<div class="hidden md:flex items-center space-x-8">
|
||||
<a href="/" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Home</a>
|
||||
<a href="/about" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">About</a>
|
||||
<a href="/events" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Events</a>
|
||||
<a href="/members" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Members</a>
|
||||
<a href="/orphanage" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Orphanage</a>
|
||||
<a href="/gallery" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Gallery</a>
|
||||
<a href="/contact" class="text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400 transition-colors">Contact</a>
|
||||
</div>
|
||||
|
||||
<!-- Right side controls -->
|
||||
<div class="hidden md:flex items-center space-x-4">
|
||||
<astro-island>
|
||||
<ThemeToggle client:load />
|
||||
</astro-island>
|
||||
<a href="/donate" class="btn bg-gradient-to-r from-ankara-red-500 to-kente-gold-500 text-white hover:shadow-lg">
|
||||
❤️ Donate
|
||||
</a>
|
||||
<a href="/join" class="btn bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 text-white hover:shadow-lg">
|
||||
⭐ Join Us
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu button -->
|
||||
<button class="md:hidden text-gray-900 dark:text-gray-200" 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">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Mobile menu -->
|
||||
<div class="md:hidden hidden" id="mobile-menu">
|
||||
<div class="mt-2 rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-gray-900/5 dark:ring-white/10 pt-4 pb-2 space-y-2">
|
||||
<a href="/" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Home</a>
|
||||
<a href="/about" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">About</a>
|
||||
<a href="/events" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Events</a>
|
||||
<a href="/members" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Members</a>
|
||||
<a href="/orphanage" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Orphanage</a>
|
||||
<a href="/gallery" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Gallery</a>
|
||||
<a href="/contact" class="block px-3 py-2 text-gray-900 dark:text-gray-200 hover:text-nigerian-green-700 dark:hover:text-kente-gold-400">Contact</a>
|
||||
<div class="pt-4 space-y-2">
|
||||
<a href="/donate" class="block btn bg-gradient-to-r from-ankara-red-500 to-kente-gold-500 text-white text-center">❤️ Donate</a>
|
||||
<a href="/join" class="block btn bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 text-white text-center">⭐ Join Us</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<!-- Main Content -->
|
||||
<main id="main" tabindex="-1">
|
||||
<slot />
|
||||
</main>
|
||||
<Footer />
|
||||
</body>
|
||||
</html>
|
||||
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
body {
|
||||
padding-top: 120px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<!-- Footer -->
|
||||
<footer class="bg-gray-900 text-white py-12">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-8">
|
||||
<div class="md:col-span-2">
|
||||
<div class="flex items-center space-x-2 mb-4">
|
||||
<div class="w-10 h-10 bg-gradient-to-br from-nigerian-green-500 to-kente-gold-500 rounded-full flex items-center justify-center">
|
||||
<span class="text-white font-bold text-lg">O</span>
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-headline font-bold text-xl">Omoluabi Foundation</div>
|
||||
<div class="text-sm text-gray-400">Netherlands</div>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-gray-300 mb-4">
|
||||
Preserving Nigerian culture and heritage while building stronger communities in the Netherlands.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Quick Links</h3>
|
||||
<ul class="space-y-2 text-sm">
|
||||
<li><a href="/about" class="text-gray-300 hover:text-white transition-colors">About Us</a></li>
|
||||
<li><a href="/events" class="text-gray-300 hover:text-white transition-colors">Events</a></li>
|
||||
<li><a href="/members" class="text-gray-300 hover:text-white transition-colors">Members</a></li>
|
||||
<li><a href="/orphanage" class="text-gray-300 hover:text-white transition-colors">Orphanage</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h3 class="font-semibold mb-4">Contact Info</h3>
|
||||
<ul class="space-y-2 text-sm text-gray-300">
|
||||
<li>📍 Amsterdam, Netherlands</li>
|
||||
<li>📞 +31 (0) 123 456 789</li>
|
||||
<li>📧 info@omoluabi.nl</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-t border-gray-800 mt-8 pt-8 text-center text-sm text-gray-400">
|
||||
<p>© 2024 Omoluabi Association Netherlands. All rights reserved.</p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
<!-- Scripts -->
|
||||
<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));
|
||||
});
|
||||
}
|
||||
|
||||
const observerOptions = { threshold: 0.1, rootMargin: '0px 0px -50px 0px' };
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(entry => { if (entry.isIntersecting) { entry.target.classList.add('visible'); } });
|
||||
}, observerOptions);
|
||||
document.querySelectorAll('[data-animate-on-scroll]').forEach(el => observer.observe(el));
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
File diff suppressed because it is too large
Load Diff
@@ -2,8 +2,57 @@
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ContactForm from '../components/ContactForm.jsx';
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1 class="text-3xl font-headline font-bold text-primary mt-12 text-center">Contact Us</h1>
|
||||
<p class="mt-4 text-center">We'd love to hear from you! Please use the form below or reach out via our contact details.</p>
|
||||
<ContactForm client:load />
|
||||
</BaseLayout>
|
||||
<!-- Page Header (modern) -->
|
||||
<section class="brand-surface relative py-20 px-4 text-center text-white overflow-hidden" data-animate-on-scroll="fade-in">
|
||||
<div class="relative z-10 max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm text-white/90">
|
||||
<a href="/" class="hover:underline">Home</a>
|
||||
<span class="opacity-60">/</span>
|
||||
<span>Contact</span>
|
||||
</div>
|
||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight">Contact Us</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">We’d love to hear from you. Use the form or our details below.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section bg-white dark:bg-gray-900" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<ContactForm client:load />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section bg-gray-50 dark:bg-gray-900" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="font-headline text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
Our <span class="text-nigerian-green-700">Contact</span> Details
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||
Feel free to reach out to us through any of the following channels.
|
||||
</p>
|
||||
<div class="w-24 h-1 bg-gradient-to-r from-nigerian-green-500 to-kente-gold-500 mx-auto rounded-full mt-6"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-8 text-center">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up">
|
||||
<div class="text-4xl text-nigerian-green-500 mb-4">📍</div>
|
||||
<h3 class="font-semibold text-xl text-gray-900 dark:text-white mb-2">Our Location</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">Amsterdam, Netherlands</p>
|
||||
<p class="text-gray-700 dark:text-gray-300">Various locations across NL</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up" style="transition-delay: 0.1s;">
|
||||
<div class="text-4xl text-kente-gold-500 mb-4">📞</div>
|
||||
<h3 class="font-semibold text-xl text-gray-900 dark:text-white mb-2">Phone Number</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">+31 (0) 123 456 789</p>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up" style="transition-delay: 0.2s;">
|
||||
<div class="text-4xl text-ankara-red-500 mb-4">📧</div>
|
||||
<h3 class="font-semibold text-xl text-gray-900 dark:text-white mb-2">Email Address</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300">info@omoluabi.nl</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
@@ -0,0 +1,82 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import DonationProgressBar from '../components/DonationProgressBar.jsx';
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<!-- Page Header -->
|
||||
<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="relative z-10 max-w-4xl mx-auto">
|
||||
<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>
|
||||
<span class="text-white/60">•</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>
|
||||
<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
|
||||
</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">
|
||||
Your generous contributions empower us to continue our work in preserving Nigerian culture, supporting our community, and making a difference in the lives of those in need.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-10 items-start">
|
||||
<div data-animate-on-scroll="slide-up">
|
||||
<h2 class="text-2xl font-headline font-bold text-nigerian-green-600 mb-4">Why Your Donation Matters</h2>
|
||||
<p class="text-gray-700 leading-relaxed mb-4">
|
||||
Every donation, no matter the size, directly impacts our programs and initiatives. From cultural events that bring our community together to vital support for our orphanage, your generosity fuels our mission.
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-gray-700 space-y-2 mb-6">
|
||||
<li>Preserving and promoting Nigerian cultural heritage.</li>
|
||||
<li>Organizing community events and welfare programs.</li>
|
||||
<li>Providing essential support to the orphanage.</li>
|
||||
<li>Empowering Nigerian youth in the Netherlands.</li>
|
||||
</ul>
|
||||
<p class="text-gray-700 leading-relaxed">
|
||||
We are committed to transparency and accountability. All funds are carefully managed and directed towards areas where they can make the most significant impact.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="sticky top-24" data-animate-on-scroll="slide-up" style="transition-delay: 0.1s;">
|
||||
<DonationProgressBar client:load />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section bg-gray-50" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="font-headline text-4xl font-bold text-kente-gold-700 mb-4">
|
||||
Other Ways to Support
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 max-w-2xl mx-auto">
|
||||
There are many ways you can contribute to our cause and make a difference.
|
||||
</p>
|
||||
<div class="w-24 h-1 bg-gradient-to-r from-kente-gold-500 to-nigerian-green-500 mx-auto rounded-full mt-6"></div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap justify-center gap-6">
|
||||
<div class="bg-white p-6 rounded-xl shadow-md border border-gray-200 flex-1 min-w-[280px] max-w-[350px]" data-animate-on-scroll="slide-up">
|
||||
<h3 class="text-xl font-semibold text-adire-blue-600 mb-3">Volunteer Your Time</h3>
|
||||
<p class="text-gray-700 mb-4">
|
||||
Lend a hand at our events or administrative tasks. Your skills are valuable!
|
||||
</p>
|
||||
<a href="/contact" class="text-adire-blue-500 hover:underline font-medium">Learn More →</a>
|
||||
</div>
|
||||
<div class="bg-white p-6 rounded-xl shadow-md border border-gray-200 flex-1 min-w-[280px] max-w-[350px]" data-animate-on-scroll="slide-up" style="transition-delay: 0.1s;">
|
||||
<h3 class="text-xl font-semibold text-ankara-red-600 mb-3">Become a Member</h3>
|
||||
<p class="text-gray-700 mb-4">
|
||||
Join our association and be part of a vibrant community.
|
||||
</p>
|
||||
<a href="/join" class="text-ankara-red-500 hover:underline font-medium">Join Us →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
@@ -1,27 +1,29 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
const events = await Astro.glob('../../content/events/*.md');
|
||||
const sortedEvents = events.sort((a, b) => new Date(b.frontmatter.date) - new Date(a.frontmatter.date));
|
||||
import EventFilterSearch from '../components/EventFilterSearch.jsx';
|
||||
|
||||
const eventFiles = Object.values(await import.meta.glob('../../content/events/*.md', { eager: true })) as any[];
|
||||
const events = eventFiles;
|
||||
const sortedEvents = events.sort((a, b) => new Date(b.frontmatter?.date || 0).getTime() - new Date(a.frontmatter?.date || 0).getTime());
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1 class="text-3xl font-headline font-bold text-primary mt-12 text-center">Events</h1>
|
||||
<p class="mt-4 text-center">See our upcoming and past events below.</p>
|
||||
<section class="max-w-5xl mx-auto mt-8 grid grid-cols-1 md:grid-cols-2 gap-8 px-4">
|
||||
{sortedEvents.map(event => (
|
||||
<article class="card bg-base-100 shadow-lg">
|
||||
<figure>
|
||||
<img src={event.frontmatter.image} alt={event.frontmatter.title} class="w-full h-48 object-cover" />
|
||||
</figure>
|
||||
<div class="card-body">
|
||||
<div class="flex items-center gap-2 mb-2">
|
||||
<span class="badge badge-secondary">{event.frontmatter.category}</span>
|
||||
<span class="text-xs text-gray-500">{new Date(event.frontmatter.date).toLocaleDateString()}</span>
|
||||
</div>
|
||||
<h2 class="card-title text-lg font-bold">{event.frontmatter.title}</h2>
|
||||
<p class="text-sm mb-2">{event.frontmatter.description}</p>
|
||||
<div class="text-xs text-gray-400">{event.compiledContent()}</div>
|
||||
</div>
|
||||
</article>
|
||||
))}
|
||||
<!-- Page Header (modern) -->
|
||||
<section class="brand-surface relative py-20 px-4 text-center text-white overflow-hidden" data-animate-on-scroll="fade-in">
|
||||
<div class="relative z-10 max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm text-white/90">
|
||||
<a href="/" class="hover:underline">Home</a>
|
||||
<span class="opacity-60">/</span>
|
||||
<span>Events</span>
|
||||
</div>
|
||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight">Events</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">Find community gatherings, cultural celebrations, and programs across the Netherlands.</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<section class="section bg-white dark:bg-gray-900" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<EventFilterSearch events={sortedEvents} client:load />
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
19
src/pages/gallery.astro
Normal file
19
src/pages/gallery.astro
Normal file
@@ -0,0 +1,19 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import ImageGallery from '../components/ImageGallery.jsx';
|
||||
|
||||
const galleryFiles = Object.values(await import.meta.glob('../../content/gallery/gallery-sample.md', { eager: true })) as any[];
|
||||
const images = galleryFiles[0]?.frontmatter?.images || [];
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<section class="brand-surface relative py-20 px-4 text-center text-white overflow-hidden" data-animate-on-scroll="fade-in">
|
||||
<div class="relative z-10 max-w-4xl mx-auto">
|
||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight">Gallery</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">Moments of culture, community, and celebration.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="max-w-6xl mx-auto py-12 px-4 bg-white dark:bg-gray-900">
|
||||
<ImageGallery images={images} client:load />
|
||||
</section>
|
||||
</BaseLayout>
|
File diff suppressed because it is too large
Load Diff
@@ -1,22 +1,38 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
const membersData = await Astro.glob('../../content/members/members-sample.md');
|
||||
const intro = membersData[0]?.frontmatter.intro || '';
|
||||
const members = membersData[0]?.frontmatter.members || [];
|
||||
const memberFiles = Object.values(await import.meta.glob('../../content/members/members-sample.md', { eager: true })) as any[];
|
||||
const intro = memberFiles[0]?.frontmatter?.intro || '';
|
||||
const members = memberFiles[0]?.frontmatter?.members || [];
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1 class="text-3xl font-headline font-bold text-primary mt-12 text-center">Meet Our Members</h1>
|
||||
<section class="max-w-3xl mx-auto mt-8 mb-12 bg-base-100 rounded-xl shadow p-6">
|
||||
<h2 class="text-xl font-bold mb-4 text-accent">Membership Benefits/Welfare Packages</h2>
|
||||
<div class="text-gray-700 whitespace-pre-line">{intro}</div>
|
||||
</section>
|
||||
<section class="max-w-6xl mx-auto grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 px-4 mb-20">
|
||||
{members.map(member => (
|
||||
<div class="card bg-base-100 shadow-lg rounded-xl flex flex-col items-center p-4">
|
||||
<img src={member.image} alt={member.name} class="w-40 h-40 object-cover rounded-lg mb-4" />
|
||||
<h3 class="font-bold text-lg text-primary mb-1">{member.name}</h3>
|
||||
<p class="text-sm text-gray-600">{member.role}</p>
|
||||
<!-- Page Header (modern) -->
|
||||
<section class="brand-surface relative py-20 px-4 text-center text-white overflow-hidden" data-animate-on-scroll="fade-in">
|
||||
<div class="relative z-10 max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm text-white/90">
|
||||
<a href="/" class="hover:underline">Home</a>
|
||||
<span class="opacity-60">/</span>
|
||||
<span>Members</span>
|
||||
</div>
|
||||
))}
|
||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight">Our Members</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">Dedicated people at the heart of Omoluabi.</p>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
||||
|
||||
<section class="max-w-3xl mx-auto mt-8 mb-12 bg-white dark:bg-gray-800 rounded-xl shadow p-6" data-animate-on-scroll="fade-in">
|
||||
<h2 class="text-xl font-bold mb-4 text-nigerian-green-700 dark:text-kente-gold-400">Membership Benefits/Welfare Packages</h2>
|
||||
<div class="text-gray-700 dark:text-gray-300 whitespace-pre-line">{intro}</div>
|
||||
</section>
|
||||
|
||||
<section class="max-w-6xl mx-auto mb-20" data-animate-on-scroll="fade-in">
|
||||
<div class="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-8 px-4">
|
||||
{Array.isArray(members) && members.map((member: any) => (
|
||||
<div class="card bg-white dark:bg-gray-800 shadow-lg rounded-xl flex flex-col items-center p-4 transform transition-transform duration-300 hover:scale-105 hover:shadow-xl">
|
||||
<img src={member.image} alt={member.name} class="w-40 h-40 object-cover rounded-lg mb-4" />
|
||||
<h3 class="font-bold text-lg text-gray-900 dark:text-white mb-1">{member.name}</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">{member.role}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
@@ -1,8 +1,89 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
---
|
||||
|
||||
<BaseLayout>
|
||||
<h1 class="text-3xl font-headline font-bold text-nigerian-green mt-12 text-center">Orphanage</h1>
|
||||
<p class="mt-4 text-center">Learn more about our affiliated orphanage and how you can help.</p>
|
||||
<!-- Orphanage info, gallery, and donation details will go here -->
|
||||
</BaseLayout>
|
||||
<!-- Page Header (modern) -->
|
||||
<section class="brand-surface relative py-20 px-4 text-center text-white overflow-hidden" data-animate-on-scroll="fade-in">
|
||||
<div class="relative z-10 max-w-4xl mx-auto">
|
||||
<div class="flex items-center justify-center gap-2 mb-4 text-sm text-white/90">
|
||||
<a href="/" class="hover:underline">Home</a>
|
||||
<span class="opacity-60">/</span>
|
||||
<span>Orphanage</span>
|
||||
</div>
|
||||
<h1 class="font-headline text-5xl md:text-6xl font-extrabold mb-4 leading-tight">Our Orphanage Program</h1>
|
||||
<p class="text-lg md:text-xl opacity-90 max-w-2xl mx-auto">How we support children with care, education, and hope.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section bg-white dark:bg-gray-900" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-12 items-center">
|
||||
<div data-animate-on-scroll="slide-up">
|
||||
<h2 class="font-headline text-3xl font-bold text-nigerian-green-700 dark:text-kente-gold-400 mb-4">About the Orphanage</h2>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
||||
The Omoluabi Foundation is proud to support [Orphanage Name], a safe haven for orphaned and vulnerable children in Nigeria. Our partnership ensures that these children receive the care, education, and support they need to thrive.
|
||||
</p>
|
||||
<p class="text-gray-700 dark:text-gray-300 leading-relaxed mb-4">
|
||||
At [Orphanage Name], children are provided with nutritious meals, comfortable shelter, access to quality education, and essential healthcare. Beyond basic needs, we strive to create a loving and nurturing environment where each child can develop their potential and build a hopeful future.
|
||||
</p>
|
||||
<ul class="list-disc list-inside text-gray-700 space-y-2">
|
||||
<li>Providing safe and nurturing environment.</li>
|
||||
<li>Ensuring access to quality education.</li>
|
||||
<li>Offering healthcare and nutritional support.</li>
|
||||
<li>Fostering personal growth and development.</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="relative" data-animate-on-scroll="slide-up" style="transition-delay: 0.1s;">
|
||||
<img src="/images/hero2.jpg" alt="Orphanage Children" class="w-full h-96 object-cover rounded-2xl shadow-xl" />
|
||||
<div class="absolute -bottom-4 -left-4 bg-kente-gold-500 text-white px-4 py-2 rounded-full font-bold shadow-lg">Hope for the Future</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="section bg-gray-50 dark:bg-gray-900" data-animate-on-scroll="fade-in">
|
||||
<div class="container">
|
||||
<div class="text-center mb-12">
|
||||
<h2 class="font-headline text-4xl font-bold text-gray-900 dark:text-white mb-4">
|
||||
How You Can <span class="text-kente-gold-700">Help</span>
|
||||
</h2>
|
||||
<p class="text-lg text-gray-600 dark:text-gray-300 max-w-2xl mx-auto">
|
||||
Your support makes a direct and meaningful impact on the lives of these children.
|
||||
</p>
|
||||
<div class="w-24 h-1 bg-gradient-to-r from-kente-gold-500 to-nigerian-green-500 mx-auto rounded-full mt-6"></div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up">
|
||||
<h3 class="font-semibold text-xl text-nigerian-green-700 mb-3">Make a Donation</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Financial contributions are crucial for covering daily operational costs, food, education, and medical expenses.
|
||||
</p>
|
||||
<a href="/donate" class="text-nigerian-green-500 hover:underline font-medium">Donate Now →</a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up" style="transition-delay: 0.1s;">
|
||||
<h3 class="font-semibold text-xl text-kente-gold-700 mb-3">Sponsor a Child</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Provide ongoing support for a child's specific needs, including education and personal development.
|
||||
</p>
|
||||
<a href="/contact" class="text-kente-gold-500 hover:underline font-medium">Learn About Sponsorship →</a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up" style="transition-delay: 0.2s;">
|
||||
<h3 class="font-semibold text-xl text-ankara-red-700 mb-3">Volunteer Your Time</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
If you are in Nigeria, consider volunteering directly at the orphanage to assist with daily activities.
|
||||
</p>
|
||||
<a href="/contact" class="text-ankara-red-500 hover:underline font-medium">Volunteer Today →</a>
|
||||
</div>
|
||||
<div class="bg-white dark:bg-gray-800 p-6 rounded-xl shadow-md border border-gray-200 dark:border-gray-700" data-animate-on-scroll="slide-up" style="transition-delay: 0.3s;">
|
||||
<h3 class="font-semibold text-xl text-adire-blue-700 mb-3">Spread the Word</h3>
|
||||
<p class="text-gray-700 dark:text-gray-300 mb-4">
|
||||
Share our mission with your friends, family, and social networks to help us reach more supporters.
|
||||
</p>
|
||||
<a href="#" class="text-adire-blue-500 hover:underline font-medium">Share Now →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</BaseLayout>
|
@@ -1,26 +1,25 @@
|
||||
/* src/styles/global.css */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700;800&family=Noto+Serif:wght@400;600;700&display=swap');
|
||||
@import "tailwindcss/base";
|
||||
@import "tailwindcss/components";
|
||||
@import "tailwindcss/utilities";
|
||||
|
||||
/* Root Variables for Nigerian Theme */
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
/* CSS Variables */
|
||||
:root {
|
||||
--nigerian-green: #16a34a;
|
||||
--nigerian-white: #ffffff;
|
||||
--kente-gold: #f59e0b;
|
||||
--adire-blue: #2563eb;
|
||||
--ankara-red: #dc2626;
|
||||
--earth-brown: #a18072;
|
||||
--adire-blue: #2563eb;
|
||||
/* Dutch Flag Colors */
|
||||
--dutch-red: #21468b;
|
||||
--dutch-white: #ffffff;
|
||||
--dutch-blue: #1e4785;
|
||||
}
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f0fdf4 100%);
|
||||
min-height: 100vh;
|
||||
/* Dark theme tokens */
|
||||
.dark {
|
||||
--dutch-white: #0b0f14;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
@@ -41,357 +40,138 @@ body {
|
||||
background: #15803d;
|
||||
}
|
||||
|
||||
/* Smooth hover transitions */
|
||||
* {
|
||||
transition: all 0.2s ease-in-out;
|
||||
/* Smooth scrolling */
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
/* Nigerian pattern overlay */
|
||||
.nigerian-pattern {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nigerian-pattern::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f59e0b' fill-opacity='0.03'%3E%3Cpath d='M20 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20zM40 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Enhanced button styles */
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--nigerian-green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-secondary {
|
||||
background: var(--kente-gold);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-accent {
|
||||
background: var(--ankara-red);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
padding: 1rem 2rem;
|
||||
font-size: 1.125rem;
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid rgba(229, 229, 229, 0.8);
|
||||
background: white;
|
||||
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
/* Input styling */
|
||||
.input, .textarea {
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e5e5e5;
|
||||
transition: all 0.3s ease;
|
||||
padding: 0.75rem 1rem;
|
||||
width: 100%;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.input:focus, .textarea:focus {
|
||||
border-color: var(--nigerian-green);
|
||||
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
padding: 0.5rem 1rem;
|
||||
font-size: 0.875rem;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge-primary {
|
||||
background: var(--nigerian-green);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-secondary {
|
||||
background: var(--kente-gold);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.badge-lg {
|
||||
padding: 0.75rem 1.25rem;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
/* Hero text animations */
|
||||
/* Animation keyframes */
|
||||
@keyframes textShine {
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.text-shine {
|
||||
background: linear-gradient(90deg, var(--nigerian-green), var(--kente-gold), var(--ankara-red), var(--nigerian-green));
|
||||
background-size: 200% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: textShine 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Animations */
|
||||
@keyframes float {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-10px); }
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
0% { transform: translateY(30px); opacity: 0; }
|
||||
100% { transform: translateY(0px); opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
0% { opacity: 0; }
|
||||
100% { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes bounceGentle {
|
||||
0%, 100% { transform: translateY(0px); }
|
||||
50% { transform: translateY(-5px); }
|
||||
0%, 20%, 53%, 80%, 100% { transform: translateY(0); }
|
||||
40%, 43% { transform: translateY(-10px); }
|
||||
70% { transform: translateY(-5px); }
|
||||
90% { transform: translateY(-2px); }
|
||||
}
|
||||
|
||||
/* Animation classes */
|
||||
.animate-text-shine {
|
||||
background: linear-gradient(90deg, #ffffff, #e0e7ff, #8b5cf6, #e0e7ff, #ffffff);
|
||||
background-size: 300% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: textShine 8s linear infinite; /* Changed from 4s to 8s */
|
||||
}
|
||||
|
||||
.animate-float {
|
||||
animation: float 6s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.animate-slide-up {
|
||||
animation: slideUp 0.8s ease-out;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fadeIn 1s ease-out;
|
||||
}
|
||||
|
||||
.animate-bounce-gentle {
|
||||
animation: bounceGentle 2s ease-in-out infinite;
|
||||
animation: bounceGentle 2s infinite;
|
||||
}
|
||||
|
||||
/* Color utilities */
|
||||
.text-nigerian-green { color: var(--nigerian-green); }
|
||||
.text-kente-gold { color: var(--kente-gold); }
|
||||
.text-ankara-red { color: var(--ankara-red); }
|
||||
.text-primary { color: var(--nigerian-green); }
|
||||
.text-secondary { color: var(--kente-gold); }
|
||||
.text-accent { color: var(--ankara-red); }
|
||||
|
||||
.bg-nigerian-green { background-color: var(--nigerian-green); }
|
||||
.bg-kente-gold { background-color: var(--kente-gold); }
|
||||
.bg-ankara-red { background-color: var(--ankara-red); }
|
||||
.bg-primary { background-color: var(--nigerian-green); }
|
||||
.bg-secondary { background-color: var(--kente-gold); }
|
||||
.bg-accent { background-color: var(--ankara-red); }
|
||||
|
||||
/* Gradient backgrounds */
|
||||
.bg-nigerian-gradient {
|
||||
background: linear-gradient(135deg, var(--nigerian-green) 0%, var(--kente-gold) 100%);
|
||||
.animation-delay-1s {
|
||||
animation-delay: 1s;
|
||||
}
|
||||
|
||||
.bg-cultural-gradient {
|
||||
background: linear-gradient(135deg, var(--nigerian-green) 0%, var(--kente-gold) 25%, var(--ankara-red) 75%, var(--nigerian-green) 100%);
|
||||
}
|
||||
/* Layout classes */
|
||||
@layer components {
|
||||
.section {
|
||||
@apply py-16;
|
||||
}
|
||||
|
||||
/* Section padding utility */
|
||||
.section-padding {
|
||||
padding: 5rem 1rem;
|
||||
}
|
||||
.container {
|
||||
@apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-padding {
|
||||
padding: 3rem 1rem;
|
||||
/* Card components */
|
||||
.card {
|
||||
@apply bg-white rounded-xl shadow-lg overflow-hidden;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
@apply p-6;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply text-xl font-bold mb-2;
|
||||
}
|
||||
|
||||
/* Button components */
|
||||
.btn {
|
||||
@apply inline-flex items-center px-6 py-3 rounded-lg font-medium transition-all duration-200 cursor-pointer;
|
||||
}
|
||||
|
||||
.btn-lg {
|
||||
@apply px-8 py-4 text-lg;
|
||||
}
|
||||
|
||||
/* Badge components */
|
||||
.badge {
|
||||
@apply inline-flex items-center px-3 py-1 rounded-full text-sm font-medium;
|
||||
}
|
||||
|
||||
.badge-lg {
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
}
|
||||
|
||||
/* Grid utilities */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 0 1rem;
|
||||
/* Custom utility classes */
|
||||
@layer utilities {
|
||||
.text-shadow-lg {
|
||||
text-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||
}
|
||||
|
||||
.backdrop-blur-xs {
|
||||
backdrop-filter: blur(2px);
|
||||
}
|
||||
}
|
||||
|
||||
/* Typography */
|
||||
.font-headline { font-family: 'Poppins', system-ui, sans-serif; }
|
||||
.font-body { font-family: 'Inter', system-ui, sans-serif; }
|
||||
.font-cultural { font-family: 'Noto Serif', serif; }
|
||||
|
||||
/* Text sizes */
|
||||
.text-xs { font-size: 0.75rem; }
|
||||
.text-sm { font-size: 0.875rem; }
|
||||
.text-base { font-size: 1rem; }
|
||||
.text-lg { font-size: 1.125rem; }
|
||||
.text-xl { font-size: 1.25rem; }
|
||||
.text-2xl { font-size: 1.5rem; }
|
||||
.text-3xl { font-size: 1.875rem; }
|
||||
.text-4xl { font-size: 2.25rem; }
|
||||
.text-5xl { font-size: 3rem; }
|
||||
.text-6xl { font-size: 3.75rem; }
|
||||
.text-7xl { font-size: 4.5rem; }
|
||||
|
||||
/* Font weights */
|
||||
.font-light { font-weight: 300; }
|
||||
.font-normal { font-weight: 400; }
|
||||
.font-medium { font-weight: 500; }
|
||||
.font-semibold { font-weight: 600; }
|
||||
.font-bold { font-weight: 700; }
|
||||
|
||||
/* Spacing utilities */
|
||||
.p-4 { padding: 1rem; }
|
||||
.p-6 { padding: 1.5rem; }
|
||||
.p-8 { padding: 2rem; }
|
||||
.px-4 { padding-left: 1rem; padding-right: 1rem; }
|
||||
.py-4 { padding-top: 1rem; padding-bottom: 1rem; }
|
||||
.m-4 { margin: 1rem; }
|
||||
.mb-4 { margin-bottom: 1rem; }
|
||||
.mb-6 { margin-bottom: 1.5rem; }
|
||||
.mb-8 { margin-bottom: 2rem; }
|
||||
.mt-8 { margin-top: 2rem; }
|
||||
.mt-12 { margin-top: 3rem; }
|
||||
.mt-16 { margin-top: 4rem; }
|
||||
.mt-20 { margin-top: 5rem; }
|
||||
|
||||
/* Layout utilities */
|
||||
.flex { display: flex; }
|
||||
.grid { display: grid; }
|
||||
.block { display: block; }
|
||||
.inline-flex { display: inline-flex; }
|
||||
.items-center { align-items: center; }
|
||||
.justify-center { justify-content: center; }
|
||||
.justify-between { justify-content: space-between; }
|
||||
.gap-4 { gap: 1rem; }
|
||||
.gap-6 { gap: 1.5rem; }
|
||||
.gap-8 { gap: 2rem; }
|
||||
.gap-12 { gap: 3rem; }
|
||||
|
||||
/* Grid utilities */
|
||||
.grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
.grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
|
||||
@media (min-width: 768px) {
|
||||
.md\\:grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.md\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
/* Animation utilities */
|
||||
[data-animate-on-scroll] {
|
||||
opacity: 0;
|
||||
transform: translateY(30px);
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.lg\\:grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
||||
.lg\\:col-span-2 { grid-column: span 2 / span 2; }
|
||||
[data-animate-on-scroll].visible {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
/* Positioning */
|
||||
.relative { position: relative; }
|
||||
.absolute { position: absolute; }
|
||||
.fixed { position: fixed; }
|
||||
.top-0 { top: 0; }
|
||||
.left-0 { left: 0; }
|
||||
.right-0 { right: 0; }
|
||||
.bottom-0 { bottom: 0; }
|
||||
.inset-0 { top: 0; right: 0; bottom: 0; left: 0; }
|
||||
.z-10 { z-index: 10; }
|
||||
.z-50 { z-index: 50; }
|
||||
/* Responsive design adjustments */
|
||||
@media (max-width: 768px) {
|
||||
.section {
|
||||
@apply py-8;
|
||||
}
|
||||
|
||||
.hero-title-shine {
|
||||
font-size: 2.5rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Width and height */
|
||||
.w-full { width: 100%; }
|
||||
.h-full { height: 100%; }
|
||||
.min-h-screen { min-height: 100vh; }
|
||||
.max-w-2xl { max-width: 42rem; }
|
||||
.max-w-3xl { max-width: 48rem; }
|
||||
.max-w-4xl { max-width: 56rem; }
|
||||
.max-w-5xl { max-width: 64rem; }
|
||||
.max-w-6xl { max-width: 72rem; }
|
||||
.mx-auto { margin-left: auto; margin-right: auto; }
|
||||
|
||||
/* Rounded corners */
|
||||
.rounded-lg { border-radius: 0.5rem; }
|
||||
.rounded-xl { border-radius: 0.75rem; }
|
||||
.rounded-2xl { border-radius: 1rem; }
|
||||
.rounded-full { border-radius: 9999px; }
|
||||
|
||||
/* Shadows */
|
||||
.shadow-lg { box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); }
|
||||
.shadow-xl { box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); }
|
||||
.shadow-2xl { box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); }
|
||||
|
||||
/* Transforms */
|
||||
.transform { transform: translateX(var(--tw-translate-x, 0)) translateY(var(--tw-translate-y, 0)) rotate(var(--tw-rotate, 0)) skewX(var(--tw-skew-x, 0)) skewY(var(--tw-skew-y, 0)) scaleX(var(--tw-scale-x, 1)) scaleY(var(--tw-scale-y, 1)); }
|
||||
.hover\\:scale-105:hover { --tw-scale-x: 1.05; --tw-scale-y: 1.05; }
|
||||
.hover\\:-translate-y-2:hover { --tw-translate-y: -0.5rem; }
|
||||
|
||||
/* Text alignment */
|
||||
.text-center { text-align: center; }
|
||||
.text-left { text-align: left; }
|
||||
|
||||
/* Overflow */
|
||||
.overflow-hidden { overflow: hidden; }
|
||||
|
||||
/* Background opacity */
|
||||
.bg-opacity-80 { background-color: rgba(var(--tw-bg-opacity-value, 1), 0.8); }
|
||||
.bg-opacity-90 { background-color: rgba(var(--tw-bg-opacity-value, 1), 0.9); }
|
||||
|
||||
/* Object fit */
|
||||
.object-cover { object-fit: cover; }
|
||||
.object-contain { object-fit: contain; }
|
||||
/* Reduced motion support */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.animate-text-shine,
|
||||
.animate-float,
|
||||
.animate-bounce-gentle {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
* {
|
||||
transition-duration: 0.01ms !important;
|
||||
animation-duration: 0.01ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
}
|
||||
}
|
@@ -211,11 +211,29 @@
|
||||
box-shadow: 0 0 20px rgba(22, 163, 74, 0.6);
|
||||
}
|
||||
|
||||
/* Nigerian Flag Colors */
|
||||
.nigerian-gradient {
|
||||
background: linear-gradient(90deg, #16a34a 0%, #ffffff 50%, #16a34a 100%);
|
||||
/* Nigerian Flag Background (vertical stripes) */
|
||||
.nigerian-flag-bg {
|
||||
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 {
|
||||
background-image:
|
||||
@@ -341,4 +359,55 @@
|
||||
.text-gray-400 {
|
||||
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%);
|
||||
}
|
||||
|
||||
/* Subtle brand surface (used for CTA sections) */
|
||||
.brand-surface {
|
||||
background: radial-gradient(1200px 600px at 20% 20%, rgba(10,10,10,0.08), rgba(10,10,10,0) 60%),
|
||||
linear-gradient(135deg, rgba(12, 84, 36, 0.95) 0%, rgba(29, 78, 216, 0.90) 100%);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .brand-surface {
|
||||
background: radial-gradient(1200px 600px at 20% 20%, rgba(255,255,255,0.04), rgba(255,255,255,0) 60%),
|
||||
linear-gradient(135deg, rgba(6, 59, 21, 0.95) 0%, rgba(23, 37, 84, 0.95) 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);
|
||||
}
|
3
tailwind-input.css
Normal file
3
tailwind-input.css
Normal file
@@ -0,0 +1,3 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
@@ -1,284 +1,87 @@
|
||||
// tailwind.config.mjs
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
// Nigerian flag colors as primary palette
|
||||
'nigerian-green': {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#22c55e',
|
||||
600: '#16a34a',
|
||||
700: '#15803d',
|
||||
800: '#166534',
|
||||
900: '#14532d',
|
||||
},
|
||||
'nigerian-white': {
|
||||
50: '#ffffff',
|
||||
100: '#fefefe',
|
||||
200: '#fafafa',
|
||||
300: '#f5f5f5',
|
||||
400: '#efefef',
|
||||
500: '#e5e5e5',
|
||||
},
|
||||
// Rich cultural colors inspired by Nigerian textiles
|
||||
'kente-gold': {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b',
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
'adire-blue': {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb',
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
'ankara-red': {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626',
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
'earth-brown': {
|
||||
50: '#fdf8f6',
|
||||
100: '#f2e8e5',
|
||||
200: '#eaddd7',
|
||||
300: '#e0cfc5',
|
||||
400: '#d2bab0',
|
||||
500: '#bfa094',
|
||||
600: '#a18072',
|
||||
700: '#977669',
|
||||
800: '#846358',
|
||||
900: '#43302b',
|
||||
}
|
||||
darkMode: 'class',
|
||||
content: ['./src/**/*.{astro,html,js,jsx,md,mdx,svelte,ts,tsx,vue}'],
|
||||
theme: {
|
||||
extend: {
|
||||
fontFamily: {
|
||||
'sans': ['Inter', 'system-ui', 'sans-serif'],
|
||||
'headline': ['Poppins', 'system-ui', 'sans-serif'],
|
||||
'serif': ['Noto Serif', 'serif'],
|
||||
},
|
||||
colors: {
|
||||
'nigerian-green': {
|
||||
50: '#f0fdf4',
|
||||
100: '#dcfce7',
|
||||
200: '#bbf7d0',
|
||||
300: '#86efac',
|
||||
400: '#4ade80',
|
||||
500: '#16a34a', // Primary Nigerian green
|
||||
600: '#15803d',
|
||||
700: '#166534',
|
||||
800: '#14532d',
|
||||
900: '#14532d',
|
||||
},
|
||||
fontFamily: {
|
||||
'headline': ['Poppins', 'system-ui', 'sans-serif'],
|
||||
'body': ['Inter', 'system-ui', 'sans-serif'],
|
||||
'cultural': ['Noto Serif', 'serif'],
|
||||
'kente-gold': {
|
||||
50: '#fffbeb',
|
||||
100: '#fef3c7',
|
||||
200: '#fde68a',
|
||||
300: '#fcd34d',
|
||||
400: '#fbbf24',
|
||||
500: '#f59e0b', // Primary gold
|
||||
600: '#d97706',
|
||||
700: '#b45309',
|
||||
800: '#92400e',
|
||||
900: '#78350f',
|
||||
},
|
||||
backgroundImage: {
|
||||
'nigerian-pattern': `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='%23f59e0b' fill-opacity='0.05'%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")`,
|
||||
'kente-gradient': 'linear-gradient(135deg, #f59e0b 0%, #dc2626 25%, #16a34a 50%, #2563eb 75%, #f59e0b 100%)',
|
||||
'hero-overlay': 'linear-gradient(135deg, rgba(22, 163, 74, 0.9) 0%, rgba(245, 158, 11, 0.8) 100%)',
|
||||
'ankara-red': {
|
||||
50: '#fef2f2',
|
||||
100: '#fee2e2',
|
||||
200: '#fecaca',
|
||||
300: '#fca5a5',
|
||||
400: '#f87171',
|
||||
500: '#ef4444',
|
||||
600: '#dc2626', // Primary red
|
||||
700: '#b91c1c',
|
||||
800: '#991b1b',
|
||||
900: '#7f1d1d',
|
||||
},
|
||||
animation: {
|
||||
'float': 'float 6s ease-in-out infinite',
|
||||
'slide-up': 'slideUp 0.8s ease-out',
|
||||
'fade-in': 'fadeIn 1s ease-out',
|
||||
'bounce-gentle': 'bounceGentle 2s ease-in-out infinite',
|
||||
'adire-blue': {
|
||||
50: '#eff6ff',
|
||||
100: '#dbeafe',
|
||||
200: '#bfdbfe',
|
||||
300: '#93c5fd',
|
||||
400: '#60a5fa',
|
||||
500: '#3b82f6',
|
||||
600: '#2563eb', // Primary blue
|
||||
700: '#1d4ed8',
|
||||
800: '#1e40af',
|
||||
900: '#1e3a8a',
|
||||
},
|
||||
keyframes: {
|
||||
float: {
|
||||
'0%, 100%': { transform: 'translateY(0px)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(30px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0px)', opacity: '1' },
|
||||
},
|
||||
fadeIn: {
|
||||
'0%': { opacity: '0' },
|
||||
'100%': { opacity: '1' },
|
||||
},
|
||||
bounceGentle: {
|
||||
'0%, 100%': { transform: 'translateY(0px)' },
|
||||
'50%': { transform: 'translateY(-5px)' },
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'text-shine': 'text-shine 8s linear infinite', // Changed from 4s to 8s
|
||||
'float': 'float 6s ease-in-out infinite',
|
||||
'bounce-gentle': 'bounce-gentle 2s infinite',
|
||||
},
|
||||
keyframes: {
|
||||
'text-shine': {
|
||||
'0%': { 'background-position': '-200% center' },
|
||||
'100%': { 'background-position': '200% center' },
|
||||
},
|
||||
'float': {
|
||||
'0%, 100%': { transform: 'translateY(0px)' },
|
||||
'50%': { transform: 'translateY(-10px)' },
|
||||
},
|
||||
'bounce-gentle': {
|
||||
'0%, 20%, 53%, 80%, 100%': { transform: 'translateY(0)' },
|
||||
'40%, 43%': { transform: 'translateY(-10px)' },
|
||||
'70%': { transform: 'translateY(-5px)' },
|
||||
'90%': { transform: 'translateY(-2px)' },
|
||||
},
|
||||
boxShadow: {
|
||||
'nigerian': '0 10px 25px -3px rgba(22, 163, 74, 0.1), 0 4px 6px -2px rgba(22, 163, 74, 0.05)',
|
||||
'kente': '0 10px 25px -3px rgba(245, 158, 11, 0.2), 0 4px 6px -2px rgba(245, 158, 11, 0.1)',
|
||||
}
|
||||
},
|
||||
},
|
||||
daisyui: {
|
||||
themes: [
|
||||
{
|
||||
omoluabi: {
|
||||
"primary": "#16a34a", // Nigerian green
|
||||
"secondary": "#f59e0b", // Kente gold
|
||||
"accent": "#dc2626", // Ankara red
|
||||
"neutral": "#fafafa", // Clean white base
|
||||
"base-100": "#ffffff", // Pure white
|
||||
"base-200": "#f5f5f5", // Light gray
|
||||
"base-300": "#e5e5e5", // Medium gray
|
||||
"info": "#2563eb", // Adire blue
|
||||
"success": "#22c55e", // Success green
|
||||
"warning": "#f59e0b", // Warning gold
|
||||
"error": "#dc2626", // Error red
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
plugins: [require("daisyui")],
|
||||
}
|
||||
|
||||
/* Updated global.css */
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700;800&family=Noto+Serif:wght@400;600;700&display=swap');
|
||||
@import "tailwindcss";
|
||||
|
||||
html {
|
||||
scroll-behavior: smooth;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background: linear-gradient(135deg, #fafafa 0%, #f0fdf4 100%);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
/* Custom scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #16a34a;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #15803d;
|
||||
}
|
||||
|
||||
/* Smooth hover transitions */
|
||||
* {
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
/* Nigerian pattern overlay */
|
||||
.nigerian-pattern {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nigerian-pattern::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image: url("data:image/svg+xml,%3Csvg width='40' height='40' viewBox='0 0 40 40' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23f59e0b' fill-opacity='0.03'%3E%3Cpath d='M20 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20zM40 20c0 11.046-8.954 20-20 20v-40c11.046 0 20 8.954 20 20z'/%3E%3C/g%3E%3C/svg%3E");
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Enhanced button styles */
|
||||
.btn {
|
||||
border-radius: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.btn::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
|
||||
transition: left 0.5s;
|
||||
}
|
||||
|
||||
.btn:hover::before {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 10px 25px -3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Card enhancements */
|
||||
.card {
|
||||
border-radius: 16px;
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border: 1px solid rgba(229, 229, 229, 0.8);
|
||||
}
|
||||
|
||||
.card:hover {
|
||||
transform: translateY(-4px);
|
||||
box-shadow: 0 20px 40px -12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* Input styling */
|
||||
.input, .textarea {
|
||||
border-radius: 12px;
|
||||
border: 2px solid #e5e5e5;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.input:focus, .textarea:focus {
|
||||
border-color: #16a34a;
|
||||
box-shadow: 0 0 0 3px rgba(22, 163, 74, 0.1);
|
||||
}
|
||||
|
||||
/* Badge styling */
|
||||
.badge {
|
||||
border-radius: 20px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
/* Hero text animations */
|
||||
@keyframes textShine {
|
||||
0% { background-position: -200% center; }
|
||||
100% { background-position: 200% center; }
|
||||
}
|
||||
|
||||
.text-shine {
|
||||
background: linear-gradient(90deg, #16a34a, #f59e0b, #dc2626, #16a34a);
|
||||
background-size: 200% auto;
|
||||
background-clip: text;
|
||||
-webkit-background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation: textShine 3s linear infinite;
|
||||
}
|
||||
|
||||
/* Loading animations */
|
||||
.animate-pulse-slow {
|
||||
animation: pulse 3s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||
}
|
||||
|
||||
/* Custom spacing utilities */
|
||||
.section-padding {
|
||||
padding: 5rem 1rem;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.section-padding {
|
||||
padding: 3rem 1rem;
|
||||
}
|
||||
}
|
||||
},
|
||||
plugins: [ ],
|
||||
}
|
Reference in New Issue
Block a user