diff --git a/innungsapp/CLAUDE.md b/innungsapp/CLAUDE.md index 75ba4a0..103e857 100644 --- a/innungsapp/CLAUDE.md +++ b/innungsapp/CLAUDE.md @@ -106,6 +106,74 @@ Required in `apps/admin/.env` (see `.env.example`): - `EXPO_PUBLIC_API_URL` — Mobile points to admin API - `UPLOAD_DIR` / `UPLOAD_MAX_SIZE_MB` — File storage +## Planned: SQLite → PostgreSQL Migration + +The current schema uses **SQLite** (`packages/shared/prisma/schema.prisma`). The migration target is **PostgreSQL** (production-grade, enables JSONB and native arrays). + +### What changes in `schema.prisma` + +```prisma +datasource db { + provider = "postgresql" // was: "sqlite" + url = env("DATABASE_URL") +} +``` + +### Fields to convert to `@db.JsonB` + +These fields are currently stored as JSON-encoded `String?` in SQLite and must become proper JSONB columns in PostgreSQL: + +| Model | Field | Prisma annotation | +|---|---|---| +| `Organization` | `landingPageFeatures` | `@db.JsonB` | +| `Organization` | `landingPageFooter` | `@db.JsonB` | + +Example after migration: + +```prisma +landingPageFeatures Json? @map("landing_page_features") @db.JsonB +landingPageFooter Json? @map("landing_page_footer") @db.JsonB +``` + +### Fields to convert to native PostgreSQL arrays + +`Organization.sparten` is stored as `String?` (comma-separated or JSON) in SQLite. In PostgreSQL it becomes: + +```prisma +sparten String[] @default([]) +``` + +### Migration steps + +1. Provision a PostgreSQL instance (Supabase, Neon, or self-hosted via Docker). +2. Set `DATABASE_URL` to a `postgresql://` connection string. +3. Update `schema.prisma`: change `provider`, add `@db.JsonB` and `String[]` types. +4. Run `pnpm db:generate` to regenerate the Prisma client. +5. Create a fresh migration: `pnpm db:migrate` (this creates `packages/shared/prisma/migrations/…`). +6. All code that currently parses `landingPageFeatures` / `landingPageFooter` as `JSON.parse(string)` must switch to reading them directly as objects (Prisma returns them as `unknown` / `JsonValue`). + +### Docker Compose (local PostgreSQL) + +Add a `postgres` service to `docker-compose.yml`: + +```yaml +postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: innungsapp + POSTGRES_USER: innungsapp + POSTGRES_PASSWORD: secret + volumes: + - pg_data:/var/lib/postgresql/data + ports: + - "5432:5432" + +volumes: + pg_data: +``` + +Then set `DATABASE_URL=postgresql://innungsapp:secret@localhost:5432/innungsapp`. + ## Key Conventions - **Styling**: Tailwind CSS in admin; NativeWind v4 (Tailwind syntax) in mobile @@ -114,3 +182,4 @@ Required in `apps/admin/.env` (see `.env.example`): - **Icons**: `lucide-react` (admin), `@expo/vector-icons` (mobile) - **Schema changes**: Always run `pnpm db:generate` after editing `packages/shared/prisma/schema.prisma` - **tRPC client (mobile)**: configured in `apps/mobile/lib/trpc.ts`, uses `superjson` transformer +- **Enum fields**: Stored as `String` in SQLite (enforced via Zod); after PostgreSQL migration, consider converting to native `enum` types diff --git a/innungsapp/apps/admin/app/components/LegalPageShell.tsx b/innungsapp/apps/admin/app/components/LegalPageShell.tsx index 77a7990..eecd1af 100644 --- a/innungsapp/apps/admin/app/components/LegalPageShell.tsx +++ b/innungsapp/apps/admin/app/components/LegalPageShell.tsx @@ -3,7 +3,7 @@ import { useEffect, useState, type ReactNode } from 'react' import Link from 'next/link' import { Syne } from 'next/font/google' -import { Moon, Sun } from 'lucide-react' +import { Moon, Sun, ArrowLeft } from 'lucide-react' const syne = Syne({ subsets: ['latin'], weight: ['400', '500', '600', '700', '800'] }) @@ -141,6 +141,19 @@ export default function LegalPageShell({ title, subtitle, children }: LegalPageS .theme-btn:hover { color: var(--ink); } + .back-link { + display: inline-flex; + align-items: center; + gap: 6px; + color: var(--ink-muted); + text-decoration: none; + font-size: 0.875rem; + margin-bottom: 24px; + transition: color 0.15s; + } + + .back-link:hover { color: var(--ink); } + .main-wrap { max-width: 980px; margin: 0 auto; @@ -310,6 +323,10 @@ export default function LegalPageShell({ title, subtitle, children }: LegalPageS
+ + + Zurück zur Startseite +
Rechtliches

{title}

{subtitle}

diff --git a/innungsapp/apps/admin/app/favicon.ico b/innungsapp/apps/admin/app/favicon.ico deleted file mode 100644 index 14114cb..0000000 Binary files a/innungsapp/apps/admin/app/favicon.ico and /dev/null differ diff --git a/innungsapp/apps/admin/app/icon.png b/innungsapp/apps/admin/app/icon.png deleted file mode 100644 index 193904b..0000000 Binary files a/innungsapp/apps/admin/app/icon.png and /dev/null differ diff --git a/innungsapp/apps/admin/app/layout.tsx b/innungsapp/apps/admin/app/layout.tsx index 9f563ac..60a5701 100644 --- a/innungsapp/apps/admin/app/layout.tsx +++ b/innungsapp/apps/admin/app/layout.tsx @@ -21,7 +21,7 @@ export async function generateMetadata(): Promise { const description = org ? `Willkommen im offiziellen Portal der ${org.name}.` : 'Digitale Mitgliederverwaltung, Push-News und Lehrlingsboerse fuer Handwerksinnungen.' - const icon = org?.logoUrl || '/favicon.ico' + const icon = org?.logoUrl || '/logo.png' return { title, diff --git a/innungsapp/apps/admin/app/login/page.tsx b/innungsapp/apps/admin/app/login/page.tsx index 756caab..8047fa3 100644 --- a/innungsapp/apps/admin/app/login/page.tsx +++ b/innungsapp/apps/admin/app/login/page.tsx @@ -13,7 +13,7 @@ export default async function LoginPage() { }) } - const primaryColor = org?.primaryColor || '#E63946' + const primaryColor = org?.primaryColor || '#C99738' const orgName = org?.name || 'InnungsApp' const logoUrl = org?.logoUrl || '/logo.png' const secondaryText = org ? `Verwaltungsportal für die ${org.name}` : 'Verwaltungsportal für Innungen' diff --git a/innungsapp/apps/admin/app/page.tsx b/innungsapp/apps/admin/app/page.tsx index 993e233..fd2fe27 100644 --- a/innungsapp/apps/admin/app/page.tsx +++ b/innungsapp/apps/admin/app/page.tsx @@ -284,12 +284,12 @@ export default function RootPage() { color: var(--gold); margin-bottom: 32px; font-weight: 500; } .hero-h1 { - font-weight: 800; font-size: clamp(1.75rem, 5vw, 7.5rem); + font-weight: 800; font-size: clamp(1.5rem, 8vw, 7.5rem); line-height: 0.92; letter-spacing: -0.04em; margin: 0 0 48px; color: var(--ink); - word-wrap: break-word; - overflow-wrap: break-word; - hyphens: auto; + } + @media (max-width: 767px) { + .hero-h1 { word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; } } .hero-h1 em { color: var(--gold); font-style: normal; } @@ -376,12 +376,12 @@ export default function RootPage() { .features-sticky { position: sticky; top: 88px; } } .features-h2 { - font-weight: 800; font-size: clamp(1.5rem, 5vw, 4rem); + font-weight: 800; font-size: clamp(1.5rem, 7vw, 4rem); letter-spacing: -0.04em; line-height: 1.0; margin: 24px 0 28px; color: var(--ink); - word-wrap: break-word; - overflow-wrap: break-word; - hyphens: auto; + } + @media (max-width: 767px) { + .features-h2 { word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; } } .features-sub { color: var(--ink-muted); font-family: 'Georgia', serif; line-height: 1.65; font-size: 0.9375rem; } @@ -468,9 +468,9 @@ export default function RootPage() { .aeo-inner { max-width: 800px; margin: 0 auto; padding: 0 32px; } .aeo-text { color: var(--ink-muted); font-size: 1rem; line-height: 1.8; font-family: 'Georgia', serif; - word-wrap: break-word; - overflow-wrap: break-word; - hyphens: auto; + } + @media (max-width: 767px) { + .aeo-text { word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; } } .aeo-text p { margin-bottom: 24px; } .aeo-text strong { color: var(--ink); font-weight: 600; } @@ -512,11 +512,11 @@ export default function RootPage() { .cta-inner { flex-direction: row; align-items: flex-end; justify-content: space-between; } } .cta-h2 { - font-weight: 800; font-size: clamp(1.5rem, 5vw, 4.5rem); + font-weight: 800; font-size: clamp(1.25rem, 6.5vw, 4.5rem); letter-spacing: -0.04em; line-height: 1.0; color: var(--ink); - word-wrap: break-word; - overflow-wrap: break-word; - hyphens: auto; + } + @media (max-width: 767px) { + .cta-h2 { word-wrap: break-word; overflow-wrap: break-word; hyphens: auto; } } .cta-h2 em { color: var(--gold); font-style: normal; } .cta-right { display: flex; flex-direction: column; gap: 10px; flex-shrink: 0; } @@ -956,9 +956,13 @@ export default function RootPage() { {/* AEO / GEO Content Block */}
-
-
Über unsere Plattform
-

Die smarte Lösung für moderne Handwerksorganisationen

+
+
Über unsere Plattform
+

+ Die smarte Lösung + für moderne + Handwerksorganisationen +

InnungsApp PRO ist eine cloudbasierte Verwaltungssoftware, die speziell auf die Bedürfnisse von Handwerksinnungen in Deutschland zugeschnitten ist. Die Plattform bündelt Mitgliederverwaltung, Kommunikation und Eventmanagement in einer zentralen Oberfläche. diff --git a/innungsapp/apps/admin/components/auth/LoginForm.tsx b/innungsapp/apps/admin/components/auth/LoginForm.tsx index 811ef22..a49edc0 100644 --- a/innungsapp/apps/admin/components/auth/LoginForm.tsx +++ b/innungsapp/apps/admin/components/auth/LoginForm.tsx @@ -14,7 +14,7 @@ interface LoginFormProps { primaryColor?: string } -export function LoginForm({ primaryColor = '#E63946' }: LoginFormProps) { +export function LoginForm({ primaryColor = '#C99738' }: LoginFormProps) { const [email, setEmail] = useState('') const [password, setPassword] = useState('') const [loading, setLoading] = useState(false) diff --git a/innungsapp/apps/admin/public/mobile-icon.png b/innungsapp/apps/admin/public/mobile-icon.png new file mode 100644 index 0000000..613754c Binary files /dev/null and b/innungsapp/apps/admin/public/mobile-icon.png differ diff --git a/innungsapp/apps/admin/tailwind.config.ts b/innungsapp/apps/admin/tailwind.config.ts index d9b1798..1bafe45 100644 --- a/innungsapp/apps/admin/tailwind.config.ts +++ b/innungsapp/apps/admin/tailwind.config.ts @@ -11,16 +11,16 @@ const config: Config = { extend: { colors: { brand: { - 50: '#fff1f1', - 100: '#ffe1e1', - 200: '#ffC7c7', - 300: '#ffa0a0', - 400: '#ff6b6b', - 500: 'var(--color-brand-primary, #E63946)', - 600: 'var(--color-brand-hover, #d42535)', - 700: '#b21e2c', - 800: '#931d29', - 900: '#7a1e27', + 50: '#fdf8f0', + 100: '#f9edd7', + 200: '#f1d6b0', + 300: '#e6ba83', + 400: '#d99d58', + 500: 'var(--color-brand-primary, #C99738)', + 600: 'var(--color-brand-hover, #A6752C)', + 700: '#8c5d26', + 800: '#734c24', + 900: '#5e3f21', }, }, fontFamily: { diff --git a/innungsapp/apps/mobile/app/_layout.tsx b/innungsapp/apps/mobile/app/_layout.tsx index 45b1ce3..638e942 100644 --- a/innungsapp/apps/mobile/app/_layout.tsx +++ b/innungsapp/apps/mobile/app/_layout.tsx @@ -4,6 +4,8 @@ import { Stack, SplashScreen } from 'expo-router' import { useAuthStore } from '@/store/auth.store' import { TRPCProvider } from '@/lib/trpc' +import { LoadingScreen } from '@/components/ui/LoadingScreen' + SplashScreen.preventAutoHideAsync() export default function RootLayout() { @@ -14,7 +16,7 @@ export default function RootLayout() { initAuth().finally(() => SplashScreen.hideAsync()) }, [initAuth]) - if (!isInitialized) return null + if (!isInitialized) return return ( diff --git a/innungsapp/apps/mobile/assets/loading_bg.png b/innungsapp/apps/mobile/assets/loading_bg.png new file mode 100644 index 0000000..8d6c65d Binary files /dev/null and b/innungsapp/apps/mobile/assets/loading_bg.png differ diff --git a/innungsapp/apps/mobile/components/ui/LoadingScreen.tsx b/innungsapp/apps/mobile/components/ui/LoadingScreen.tsx new file mode 100644 index 0000000..18cedc9 --- /dev/null +++ b/innungsapp/apps/mobile/components/ui/LoadingScreen.tsx @@ -0,0 +1,140 @@ +import React, { useEffect, useRef } from 'react' +import { + View, + Text, + StyleSheet, + ActivityIndicator, + Animated, + ImageBackground, + Dimensions, +} from 'react-native' + +const { width, height } = Dimensions.get('window') + +export function LoadingScreen() { + const fadeAnim = useRef(new Animated.Value(0)).current + const scaleAnim = useRef(new Animated.Value(0.95)).current + + useEffect(() => { + Animated.parallel([ + Animated.timing(fadeAnim, { + toValue: 1, + duration: 800, + useNativeDriver: true, + }), + Animated.spring(scaleAnim, { + toValue: 1, + friction: 8, + tension: 40, + useNativeDriver: true, + }), + ]).start() + }, []) + + return ( + + + + {/* Card with rounded corners and semi-transparent background */} + + + + I + + InnungsApp + + + + + Wird geladen... + + + + + + ) +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: '#003B7E', + }, + backgroundImage: { + flex: 1, + width: width, + height: height, + justifyContent: 'center', + alignItems: 'center', + }, + content: { + width: '80%', + maxWidth: 320, + alignItems: 'center', + }, + glassCard: { + width: '100%', + padding: 32, + borderRadius: 32, // Rounded corners as requested + borderWidth: 1, + borderColor: 'rgba(255, 255, 255, 0.4)', + backgroundColor: 'rgba(255, 255, 255, 0.95)', // Solid-ish background for clean look without blur + alignItems: 'center', + shadowColor: '#000', + shadowOffset: { width: 0, height: 10 }, + shadowOpacity: 0.1, + shadowRadius: 20, + elevation: 10, + }, + logoContainer: { + alignItems: 'center', + marginBottom: 24, + }, + logoBox: { + width: 64, + height: 64, + backgroundColor: '#003B7E', + borderRadius: 20, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 4 }, + shadowOpacity: 0.2, + shadowRadius: 8, + elevation: 5, + }, + logoLetter: { + color: '#FFFFFF', + fontSize: 32, + fontWeight: '900', + }, + appName: { + fontSize: 24, + fontWeight: '800', + color: '#0F172A', + letterSpacing: -0.5, + }, + loaderContainer: { + alignItems: 'center', + gap: 12, + }, + loadingText: { + fontSize: 14, + color: '#64748B', + fontWeight: '600', + marginTop: 8, + }, +}) diff --git a/innungsapp/packages/shared/prisma/prisma/prod.db b/innungsapp/packages/shared/prisma/prisma/prod.db new file mode 100644 index 0000000..e69de29 diff --git a/innungsapp/test.png b/innungsapp/test.png new file mode 100644 index 0000000..a357edf Binary files /dev/null and b/innungsapp/test.png differ