mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 21:21:24 +02:00
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:
parent
da6dd4ecb8
commit
df0b849408
39 changed files with 2171 additions and 4 deletions
|
|
@ -20,7 +20,9 @@
|
|||
"./themes/manadeck": "./src/themes/manadeck.css",
|
||||
"./themes/picture": "./src/themes/picture.css",
|
||||
"./themes/chat": "./src/themes/chat.css",
|
||||
"./themes/zitare": "./src/themes/zitare.css"
|
||||
"./themes/zitare": "./src/themes/zitare.css",
|
||||
"./themes/org-classic": "./src/themes/org-classic.css",
|
||||
"./themes/org-warm": "./src/themes/org-warm.css"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
|
|
|
|||
127
packages/shared-landing-ui/src/sections/ContactSection.astro
Normal file
127
packages/shared-landing-ui/src/sections/ContactSection.astro
Normal 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>
|
||||
78
packages/shared-landing-ui/src/sections/TeamSection.astro
Normal file
78
packages/shared-landing-ui/src/sections/TeamSection.astro
Normal 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>
|
||||
24
packages/shared-landing-ui/src/themes/org-classic.css
Normal file
24
packages/shared-landing-ui/src/themes/org-classic.css
Normal 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;
|
||||
}
|
||||
24
packages/shared-landing-ui/src/themes/org-warm.css
Normal file
24
packages/shared-landing-ui/src/themes/org-warm.css
Normal 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;
|
||||
}
|
||||
|
|
@ -19,6 +19,9 @@ export * from './common';
|
|||
// Contact types for cross-app integration
|
||||
export * from './contact';
|
||||
|
||||
// Landing page configuration types
|
||||
export * from './landing-config';
|
||||
|
||||
// API types
|
||||
export interface User {
|
||||
id: string;
|
||||
|
|
|
|||
90
packages/shared-types/src/landing-config.ts
Normal file
90
packages/shared-types/src/landing-config.ts
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
/**
|
||||
* Landing Page Configuration Types
|
||||
*
|
||||
* Used by the Admin UI (Manacore Web) and the Landing Builder Service
|
||||
* to configure and generate static Astro landing pages for organizations.
|
||||
*/
|
||||
|
||||
// --- Section Configs ---
|
||||
|
||||
export interface LandingHeroConfig {
|
||||
title: string;
|
||||
subtitle: string;
|
||||
variant?: 'default' | 'centered' | 'fullwidth';
|
||||
primaryCta?: { text: string; href: string };
|
||||
secondaryCta?: { text: string; href: string };
|
||||
image?: { src: string; alt: string };
|
||||
}
|
||||
|
||||
export interface LandingAboutFeature {
|
||||
icon: string;
|
||||
title: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface LandingAboutConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
features: LandingAboutFeature[];
|
||||
columns?: 2 | 3;
|
||||
}
|
||||
|
||||
export interface LandingTeamMember {
|
||||
name: string;
|
||||
role: string;
|
||||
image?: string;
|
||||
bio?: string;
|
||||
}
|
||||
|
||||
export interface LandingTeamConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
members: LandingTeamMember[];
|
||||
}
|
||||
|
||||
export interface LandingContactConfig {
|
||||
title: string;
|
||||
subtitle?: string;
|
||||
email?: string;
|
||||
phone?: string;
|
||||
address?: string;
|
||||
}
|
||||
|
||||
export interface LandingFooterLink {
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface LandingFooterConfig {
|
||||
copyright?: string;
|
||||
links?: LandingFooterLink[];
|
||||
socialLinks?: Array<{ platform: string; href: string }>;
|
||||
}
|
||||
|
||||
// --- Main Config ---
|
||||
|
||||
export type LandingTheme = 'classic' | 'warm';
|
||||
|
||||
export type LandingBuildStatus = 'success' | 'failed' | 'building';
|
||||
|
||||
export interface LandingCustomColors {
|
||||
primary?: string;
|
||||
primaryHover?: string;
|
||||
primaryGlow?: string;
|
||||
}
|
||||
|
||||
export interface LandingPageConfig {
|
||||
enabled: boolean;
|
||||
theme: LandingTheme;
|
||||
customColors?: LandingCustomColors;
|
||||
sections: {
|
||||
hero: LandingHeroConfig;
|
||||
about: LandingAboutConfig;
|
||||
team: LandingTeamConfig;
|
||||
contact: LandingContactConfig;
|
||||
footer: LandingFooterConfig;
|
||||
};
|
||||
lastBuiltAt?: string;
|
||||
lastBuildStatus?: LandingBuildStatus;
|
||||
publishedUrl?: string;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue