feat: add org landing page builder service

New service that generates static Astro landing pages for organizations
and deploys them to Cloudflare Pages at {slug}.mana.how.

Components:
- Landing Builder Service (NestJS, port 3030) with Astro template
- Admin UI in Manacore web dashboard at /organizations/[id]/landing
- TeamSection + ContactSection for shared-landing-ui
- Two org themes (classic dark, warm light)
- LandingPageConfig types in shared-types
- Docker + CI/CD integration for Mac Mini deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-03-23 13:20:10 +01:00
parent da6dd4ecb8
commit df0b849408
39 changed files with 2171 additions and 4 deletions

View file

@ -0,0 +1,127 @@
---
/**
* Shared Contact Section component
* Displays contact information (email, phone, address) in a clean layout.
*/
import Container from '../atoms/Container.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
import Card from '../atoms/Card.astro';
interface Props {
title: string;
subtitle?: string;
email?: string;
phone?: string;
address?: string;
class?: string;
id?: string;
}
const { title, subtitle, email, phone, address, class: className = '', id } = Astro.props;
const hasContactInfo = email || phone || address;
---
<section id={id} class:list={['py-16 md:py-24', className]}>
<Container size="md">
<SectionHeader title={title} subtitle={subtitle} />
{
hasContactInfo && (
<div class="max-w-2xl mx-auto">
<Card variant="bordered" padding="lg">
<div class="space-y-6">
{email && (
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-[var(--color-primary)] bg-opacity-10 flex items-center justify-center flex-shrink-0">
<svg
class="w-5 h-5 text-[var(--color-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"
/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-[var(--color-text-muted)] mb-1">E-Mail</p>
<a
href={`mailto:${email}`}
class="text-[var(--color-text-primary)] hover:text-[var(--color-primary)] transition-colors"
>
{email}
</a>
</div>
</div>
)}
{phone && (
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-[var(--color-primary)] bg-opacity-10 flex items-center justify-center flex-shrink-0">
<svg
class="w-5 h-5 text-[var(--color-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z"
/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-[var(--color-text-muted)] mb-1">Telefon</p>
<a
href={`tel:${phone.replace(/\s/g, '')}`}
class="text-[var(--color-text-primary)] hover:text-[var(--color-primary)] transition-colors"
>
{phone}
</a>
</div>
</div>
)}
{address && (
<div class="flex items-start gap-4">
<div class="w-10 h-10 rounded-lg bg-[var(--color-primary)] bg-opacity-10 flex items-center justify-center flex-shrink-0">
<svg
class="w-5 h-5 text-[var(--color-primary)]"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
/>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
/>
</svg>
</div>
<div>
<p class="text-sm font-medium text-[var(--color-text-muted)] mb-1">Adresse</p>
<p class="text-[var(--color-text-primary)] whitespace-pre-line">{address}</p>
</div>
</div>
)}
</div>
</Card>
</div>
)
}
</Container>
</section>

View file

@ -0,0 +1,78 @@
---
/**
* Shared Team Section component
* Displays team/board members in a responsive grid with avatar, name, role, and optional bio.
*/
import Container from '../atoms/Container.astro';
import SectionHeader from '../atoms/SectionHeader.astro';
import Card from '../atoms/Card.astro';
interface TeamMember {
name: string;
role: string;
image?: string;
bio?: string;
}
interface Props {
title: string;
subtitle?: string;
members: TeamMember[];
columns?: 2 | 3 | 4;
class?: string;
id?: string;
}
const { title, subtitle, members, columns = 3, class: className = '', id } = Astro.props;
const gridCols = {
2: 'md:grid-cols-2',
3: 'md:grid-cols-2 lg:grid-cols-3',
4: 'md:grid-cols-2 lg:grid-cols-4',
};
function getInitials(name: string): string {
return name
.split(' ')
.map((part) => part[0])
.join('')
.toUpperCase()
.slice(0, 2);
}
---
<section id={id} class:list={['py-16 md:py-24', className]}>
<Container>
<SectionHeader title={title} subtitle={subtitle} />
<div class:list={['grid gap-6 md:gap-8', gridCols[columns]]}>
{
members.map((member) => (
<Card variant="hover" padding="lg">
<div class="flex flex-col items-center text-center">
{member.image ? (
<img
src={member.image}
alt={member.name}
class="w-20 h-20 rounded-full object-cover mb-4"
loading="lazy"
/>
) : (
<div class="w-20 h-20 rounded-full bg-[var(--color-primary)] flex items-center justify-center mb-4">
<span class="text-xl font-bold text-white">{getInitials(member.name)}</span>
</div>
)}
<h3 class="text-lg font-semibold text-[var(--color-text-primary)]">{member.name}</h3>
<p class="text-sm text-[var(--color-primary)] font-medium mt-1">{member.role}</p>
{member.bio && (
<p class="text-sm text-[var(--color-text-secondary)] mt-3 leading-relaxed">
{member.bio}
</p>
)}
</div>
</Card>
))
}
</div>
</Container>
</section>

View file

@ -0,0 +1,24 @@
/**
* Org Classic Theme - Professional Dark
* Neutral slate tones, clean and serious. Ideal for institutions, associations, businesses.
*/
:root {
/* Primary colors - Slate Blue */
--color-primary: #64748b;
--color-primary-hover: #475569;
--color-primary-glow: rgba(100, 116, 139, 0.3);
/* Text colors */
--color-text-primary: #f1f5f9;
--color-text-secondary: #cbd5e1;
--color-text-muted: #64748b;
/* Background colors */
--color-background-page: #0f172a;
--color-background-card: #1e293b;
--color-background-card-hover: #334155;
/* Border colors */
--color-border: #334155;
--color-border-hover: #475569;
}

View file

@ -0,0 +1,24 @@
/**
* Org Warm Theme - Inviting Light
* Warm amber tones on light backgrounds. Ideal for schools, social clubs, community orgs.
*/
:root {
/* Primary colors - Amber */
--color-primary: #d97706;
--color-primary-hover: #b45309;
--color-primary-glow: rgba(217, 119, 6, 0.2);
/* Text colors */
--color-text-primary: #1c1917;
--color-text-secondary: #44403c;
--color-text-muted: #78716c;
/* Background colors */
--color-background-page: #fafaf9;
--color-background-card: #ffffff;
--color-background-card-hover: #f5f5f4;
/* Border colors */
--color-border: #e7e5e4;
--color-border-hover: #d6d3d1;
}