Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# Database
DATABASE_URL=postgresql://user:password@localhost:5432/aiabasd

# NextAuth
NEXTAUTH_SECRET=changeme
NEXTAUTH_URL=http://localhost:3000
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
LINKEDIN_CLIENT_ID=
LINKEDIN_CLIENT_SECRET=

# App
NEXT_PUBLIC_APP_URL=http://localhost:3000
SITE_BASE_URL=http://localhost:3000

# i18n
DEFAULT_LOCALE=ar

# Strapi CMS
STRAPI_URL=http://localhost:1337
STRAPI_API_TOKEN=your_strapi_readonly_token

# Search (Algolia or Meilisearch)
NEXT_PUBLIC_ALGOLIA_APP_ID=
NEXT_PUBLIC_ALGOLIA_SEARCH_KEY=
ALGOLIA_ADMIN_KEY=
MEILISEARCH_HOST=
MEILISEARCH_API_KEY=

# reCAPTCHA v3
RECAPTCHA_SITE=
RECAPTCHA_SECRET=

# Analytics
NEXT_PUBLIC_GA_ID=
NEXT_PUBLIC_POSTHOG_KEY=
NEXT_PUBLIC_POSTHOG_HOST=https://app.posthog.com

# Storage (S3-compatible)
STORAGE_BUCKET=
STORAGE_ACCESS_KEY_ID=
STORAGE_SECRET_ACCESS_KEY=
STORAGE_ENDPOINT=
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: CI

on:
push:
branches: [ main ]
pull_request:

jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'

- name: Install pnpm
run: npm i -g pnpm

- name: Install deps
run: pnpm i

- name: Typecheck
run: pnpm typecheck

- name: Lint
run: pnpm lint

- name: Unit tests
run: pnpm test

- name: E2E tests (Playwright)
run: pnpm e2e
110 changes: 110 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# AIABASD / AIBA Investor Platform

Trilingual (Arabic default, English and French mirrors) investor website for African International Business Alliance & Sustainable Development (AIABASD/AIBA).

## Tech Stack

- Next.js 15 (App Router, RSC) + TypeScript
- Tailwind CSS (RTL-aware)
- next-intl for i18n (ar, en, fr)
- Prisma + PostgreSQL
- NextAuth (gated investor portal)
- Meilisearch or Algolia (search index scaffolding)
- React Hook Form + Zod (forms)
- PDF generation via pdf-lib (LOI, AR/EN/FR)
- Analytics: GA4, PostHog
- Tests: Playwright (E2E), Vitest (unit)
- CI: GitHub Actions (typecheck, lint, unit, e2e)

## AIO SEO

Central module `lib/seo.ts` provides:
- Localized titles/descriptions (safe limits), canonical, hreflang (ar, en, fr, x-default→/en)
- Open Graph + Twitter tags with localized alt
- JSON-LD helpers for Organization, WebSite, BreadcrumbList, Offer, Article, Place
- Sitemaps:
- Next sitemap (app/sitemap.ts)
- Custom sitemap with `<xhtml:link>` alternates (app/sitemap.xml/route.ts)
- Robots: app/robots.txt

## Getting Started

1. Clone and install:
- pnpm i

2. Environment variables:
- Copy .env.example to .env.local
- Set DATABASE_URL (Postgres)
- Set NEXTAUTH_SECRET and NEXTAUTH_URL
- Set NEXT_PUBLIC_APP_URL and SITE_BASE_URL
- Optional: MEILISEARCH_* or ALGOLIA_* keys, RECAPTCHA_* keys, analytics keys.

3. Database and seed:
- pnpm prisma:generate
- pnpm prisma:migrate
- pnpm db:seed

4. Run:
- pnpm dev
- Visit http://localhost:3000 (auto-redirects to /ar)

5. Build and start:
- pnpm build && pnpm start

6. Tests:
- pnpm test
- pnpm e2e

## Routes

- /ar (default), /en, /fr
- Home, Opportunities (list + detail), Countries, Investor Portal (intent)
- API: /api/opportunities, /api/investor/intent, /api/investor/intent/[id]/pdf
- SEO: /sitemap.xml, /robots.txt

## Acceptance Criteria coverage (MVP)

1) Languages:
- Arabic RTL default; EN/FR LTR; language toggle persists (URL-based).

2) Opportunities:
- Server-side filters and text search (Prisma). Also /api/opportunities.
- Detail pages show localized fields and docs.

3) Investor flow:
- Intent form posts to /api/investor/intent with rate limit + reCAPTCHA hook.
- Generates LOI PDF localized (AR/EN/FR) at /api/investor/intent/:id/pdf.

4) Country Profiles:
- 3 demo profiles, each with ≥5 indicators and a map placeholder.

5) SEO:
- Canonical + hreflang via lib/seo.ts generateMetadata.
- JSON-LD for Organization/WebSite on home; Breadcrumb + Offer on opportunity.
- Sitemaps and robots included.

6) Security:
- CSP, clickjacking, content-type, permissions headers via middleware.
- Basic API rate limiting.
- reCAPTCHA v3 hook (requires secrets for production).

7) Tests/CI:
- Playwright smoke tests for home and opportunities list.
- Vitest unit for i18n RTL utility.
- GitHub Actions workflow runs typecheck, lint, unit, e2e.

## Deployment (Vercel)

- Configure environment variables in Vercel project.
- Set SITE_BASE_URL to your production domain.
- Middleware handles locale redirects and portal gating.

## Seed Content

Seed script populates:
- 6 opportunities (energy, agriculture, healthcare, education, infrastructure, tourism) AR/EN/FR
- 3 partners
- 3 news posts AR/EN/FR
- 3 country profiles AR/EN/FR

Arabic is the source of truth with English and French mirrors.
45 changes: 45 additions & 0 deletions README_STRAPI.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# Strapi Content Types for AIABASD / AIBA

This folder contains Strapi v4 content-type and component schemas to power the CMS for the investor platform.

## Structure

- content-types
- opportunity: AR/EN/FR, slug (uid), summaries, sector/country, ticket sizes, stage, SDGs, ESG score, KPIs, documents, hero image, SEO
- partner: name, type, country, logo, link, SEO
- country-profile: iso2, names AR/EN/FR, summary, indicators (component), SEO
- news-post: titles AR/EN/FR, slug (uid), rich bodies AR/EN/FR, published_at, SEO
- components/common
- indicator: key/value pairs
- kpi: label/value pairs
- seo: AIO SEO fields per locale, og_image, noindex/nofollow, schema_type

## How to use

1) Create a Strapi v4 project:
- npx create-strapi-app@latest cms --quickstart

2) Copy these files into your Strapi project:
- Place content-types JSON under: cms/src/api/{type}/content-types/{type}/schema.json
- For example: cms/src/api/opportunity/content-types/opportunity/schema.json
- Place components JSON under: cms/src/components/common/{component}.json

3) Rebuild Strapi:
- cd cms
- npm run develop
- Visit http://localhost:1337/admin
- Create an admin user
- Verify collection types and components exist

4) Configure media:
- Set S3-compatible provider (optional) for uploads.

5) Connect Next.js:
- Expose Read-only API tokens in Strapi Settings → API Tokens.
- In Next.js, set ENV for STRAPI_URL and STRAPI_API_TOKEN, then fetch via REST or GraphQL.

## Notes

- SEO fields align with the AIO SEO module in Next.js.
- Slug uses title_en as source to create a canonical cluster with hreflang alternates.
- Add relations if needed (e.g., linking opportunities to partners).
67 changes: 67 additions & 0 deletions app/(auth)/login/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
'use client'

import { useState } from 'react'
import { supabase } from '@/lib/supabase/client'

export default function LoginPage() {
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)

const onSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setLoading(true)
setError(null)
const { error } = await supabase.auth.signInWithPassword({ email, password })
if (error) {
setError(error.message)
} else {
window.location.href = '/dashboard'
}
setLoading(false)
}

return (
<div className="container max-w-md py-12">
<h1 className="text-2xl font-semibold">Welcome back</h1>
<p className="mt-2 text-neutral-600">Sign in to your account</p>

<form onSubmit={onSubmit} className="mt-6 space-y-4">
<div>
<label className="text-sm text-neutral-600">Email</label>
<input
type="email"
className="mt-1 w-full rounded border px-3 py-2"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="you@example.com"
required
/>
</div>
<div>
<label className="text-sm text-neutral-600">Password</label>
<input
type="password"
className="mt-1 w-full rounded border px-3 py-2"
value={password}
onChange={(e) => setPassword(e.target.value)}
placeholder="••••••••"
required
/>
</div>
{error && <p className="text-sm text-error">{error}</p>}
<button
type="submit"
className="w-full rounded bg-primary-blue px-3 py-2 font-medium text-white hover:opacity-90 disabled:opacity-50"
disabled={loading}
>
{loading ? 'Signing in...' : 'Sign In'}
</button>
<div className="text-center text-sm text-neutral-600">
Don&apos;t have an account? <a href="/register" className="text-primary-blue hover:underline">Create one</a>
</div>
</form>
</div>
)
}
Loading
Loading