This commit is contained in:
2026-04-21 07:37:18 +03:30
commit 96a79795c1
39 changed files with 3625 additions and 0 deletions

3
.env.example Normal file
View File

@@ -0,0 +1,3 @@
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/academy_engel"
NEXTAUTH_SECRET="replace-with-a-long-random-secret"
NEXTAUTH_URL="http://localhost:3000"

32
.gitignore vendored Normal file
View File

@@ -0,0 +1,32 @@
# dependencies
node_modules/
# next.js
.next/
out/
# production
build/
dist/
# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# env files
.env
.env.local
.env.development.local
.env.test.local
.env.production.local
# prisma
prisma/dev.db
prisma/dev.db-journal
# misc
.DS_Store
Thumbs.db
*.tsbuildinfo

271
IMPLEMENTATION_PLAN.md Normal file
View File

@@ -0,0 +1,271 @@
# Academy Engel Implementation Plan
This document is the working implementation checklist for the `academy engel` project.
## Product Goal
Build Academy Engel as a single full-stack Next.js application with:
- App Router
- PostgreSQL
- Prisma ORM
- bilingual public website (`en`, `de`)
- admin CMS with role-based access
- lead capture for contact and placement requests
Phase 1 does **not** include:
- student accounts
- payment flow
- enrollment automation
- learning portal features
## Current Status
### Completed
- Next.js public site structure created with App Router
- locale-based public routes added under `/{locale}`
- homepage recreated in Next.js based on `index.html`
- local image assets downloaded into `public/images`
- reusable public components added
- FAQ interaction added on the frontend
- Tailwind moved to local project setup
- `.gitignore` added
- initial Prisma schema draft added
- `.env.example` added
- `npm run build` passes on the current frontend structure
### Not Completed Yet
- Prisma runtime installation and database wiring
- actual PostgreSQL connection
- seed script with real initial content
- auth system
- admin CMS
- CRUD actions
- lead persistence
- route protection
- public pages reading from database instead of mock content
## Phase 1: Environment and Infrastructure
### 1. Package and Tooling Setup
- [ ] Run a clean install for all dependencies in `package.json`
- [ ] Confirm `next-auth`, `prisma`, `@prisma/client`, `bcryptjs`, and `zod` are installed correctly
- [ ] Resolve the `@next/swc` version mismatch warning
- [ ] Verify `postcss` and Tailwind setup is stable in dev and build modes
### 2. Environment Variables
- [ ] Create a real `.env.local` from `.env.example`
- [ ] Set `DATABASE_URL` for PostgreSQL
- [ ] Set `NEXTAUTH_SECRET`
- [ ] Set `NEXTAUTH_URL`
- [ ] Document local development env requirements
### 3. Database Bootstrapping
- [ ] Create PostgreSQL database for local development
- [ ] Run `prisma generate`
- [ ] Create first Prisma migration
- [ ] Run `prisma migrate dev`
- [ ] Confirm schema is applied successfully
## Phase 2: Database and Data Layer
### 4. Finalize Prisma Schema
- [ ] Review and refine `User`
- [ ] Review and refine `Course`
- [ ] Review and refine `Teacher`
- [ ] Review and refine `Post`
- [ ] Review and refine `FaqItem`
- [ ] Review and refine `Lead`
- [ ] Review and refine `SiteSetting`
- [ ] Add timestamps and indexes where needed
- [ ] Confirm status enums and role enums are final for phase 1
### 5. Shared Server Utilities
- [ ] Add Prisma client singleton helper
- [ ] Add auth helper utilities
- [ ] Add role guard helpers
- [ ] Add shared form validation schemas with `zod`
- [ ] Add content mapping helpers for `en` and `de`
### 6. Seed Data
- [ ] Replace placeholder seed script in `prisma/seed.mjs`
- [ ] Seed admin user
- [ ] Seed editor user
- [ ] Seed courses
- [ ] Seed teachers
- [ ] Seed blog posts
- [ ] Seed FAQ items
- [ ] Seed site settings
- [ ] Ensure seeded content matches the current homepage direction
## Phase 3: Public Website Integration
### 7. Replace Mock Content with Database Reads
- [ ] Remove dependency on static content in `lib/site-data.js` for production data access
- [ ] Fetch homepage content from Prisma
- [ ] Fetch courses list from Prisma
- [ ] Fetch course detail pages from Prisma
- [ ] Fetch teachers list from Prisma
- [ ] Fetch blog list from Prisma
- [ ] Fetch blog detail pages from Prisma
- [ ] Fetch FAQ content from Prisma
- [ ] Fetch site settings from Prisma
### 8. Public Route Behavior
- [ ] Keep `/{locale}` routing as the public structure
- [ ] Ensure both `en` and `de` render correct localized content
- [ ] Confirm metadata is generated per locale/page
- [ ] Handle not-found states for missing course/blog slugs
- [ ] Add stable navigation behavior between locales
### 9. Public Forms
- [ ] Connect contact form to `Lead` table
- [ ] Connect placement-test form to `Lead` table
- [ ] Validate form submissions with `zod`
- [ ] Store `locale` on lead records
- [ ] Store `type` as `CONTACT` or `PLACEMENT`
- [ ] Add success and failure UI states
## Phase 4: Authentication and Authorization
### 10. Auth Setup
- [ ] Configure Auth.js / NextAuth for App Router
- [ ] Add credentials provider
- [ ] Store password hashes with `bcryptjs`
- [ ] Implement login page for admin area
- [ ] Implement session handling
- [ ] Implement logout flow
### 11. Role-Based Access
- [ ] Define `ADMIN` and `EDITOR` permissions clearly
- [ ] Protect all `/admin` routes
- [ ] Restrict user management to `ADMIN`
- [ ] Allow content management for `ADMIN` and `EDITOR`
- [ ] Prevent unauthorized access to server actions
## Phase 5: Admin CMS
### 12. Admin App Structure
- [ ] Create `/admin` layout
- [ ] Add sidebar/navigation
- [ ] Add dashboard overview page
- [ ] Add shared admin form styles/components
- [ ] Add loading and empty states
### 13. Content Management Screens
- [ ] Courses list/create/edit/publish UI
- [ ] Teachers list/create/edit/publish UI
- [ ] Blog posts list/create/edit/publish UI
- [ ] FAQ list/create/edit UI
- [ ] Site settings editor
- [ ] Leads list/detail/update status UI
- [ ] Users list/create/edit UI
### 14. Admin CRUD Backend
- [ ] Create server actions for courses
- [ ] Create server actions for teachers
- [ ] Create server actions for blog posts
- [ ] Create server actions for FAQ items
- [ ] Create server actions for site settings
- [ ] Create server actions for leads
- [ ] Create server actions for users
- [ ] Add revalidation after mutations
## Phase 6: Quality, UX, and Cleanup
### 15. Frontend Cleanup
- [ ] Compare homepage visually against `index.html`
- [ ] Tighten spacing, typography, and section order where needed
- [ ] Improve mobile navigation behavior
- [ ] Improve button/link consistency
- [ ] Decide whether any remaining animations should stay CSS-only or be expanded
### 16. Content and Encoding Cleanup
- [ ] Ensure no broken encoding remains anywhere in UI or data
- [ ] Normalize copy across English and German content
- [ ] Replace any temporary ASCII-only fallback text if desired
- [ ] Make sure CMS fields support the intended localized content format
### 17. Codebase Cleanup
- [ ] Remove obsolete prototype code
- [ ] Remove unused dependencies if any
- [ ] Split oversized files if they become hard to maintain
- [ ] Keep server and client boundaries clean
- [ ] Review naming consistency across routes, models, and UI
## Testing Checklist
### 18. Technical Verification
- [ ] `npm run build` passes
- [ ] Prisma client generates successfully
- [ ] Prisma migrations run on a clean database
- [ ] Seed script runs successfully
- [ ] No broken route imports or alias issues
### 19. Public Website Verification
- [ ] `/en` renders correctly
- [ ] `/de` renders correctly
- [ ] course listing and detail pages work
- [ ] blog listing and detail pages work
- [ ] teachers page works
- [ ] FAQ page works
- [ ] contact page works
- [ ] placement page works
- [ ] all local images load correctly
### 20. Auth and Admin Verification
- [ ] admin can sign in
- [ ] editor can sign in
- [ ] anonymous user cannot access admin
- [ ] admin can manage users
- [ ] editor cannot manage users
- [ ] admin and editor can manage content
### 21. Lead Verification
- [ ] contact form creates a `Lead`
- [ ] placement form creates a `Lead`
- [ ] lead locale is stored correctly
- [ ] lead status can be updated in admin
## Recommended Execution Order
1. Finish dependency installation and fix package/runtime issues
2. Connect PostgreSQL and Prisma
3. Write real seed data
4. Replace frontend mock data with database reads
5. Wire contact and placement forms to the database
6. Add authentication
7. Build admin layout and content CRUD
8. Add role protections and user management
9. Final visual cleanup and testing
## Important Notes
- The current homepage implementation is a frontend milestone, not the final data architecture.
- The current `lib/site-data.js` should be treated as temporary mock content until Prisma-backed queries replace it.
- The CMS can be visually simple in phase 1, but permissions and data integrity must be correct.
- Media storage is external-URL based in the original plan, but local copies of current marketing images are now included for the public demo.

View File

@@ -0,0 +1,41 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getBlogPostBySlug, isSupportedLocale, locales } from "@/lib/site-data";
const blogSlugs = ["german-articles-hacks", "meldezettel-vienna", "osd-vs-goethe"];
export function generateStaticParams() {
return locales.flatMap((locale) => blogSlugs.map((slug) => ({ locale, slug })));
}
export default async function BlogDetailPage({ params }) {
const { locale, slug } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const post = getBlogPostBySlug(locale, slug);
if (!post) {
notFound();
}
return (
<main className="min-h-screen bg-white text-gray-800">
<article className="mx-auto max-w-4xl px-6 py-16">
<Link href={`/${locale}/blog`} className="text-sm font-semibold text-brand-purple">
{locale === "de" ? "Zurück zum Blog" : "Back to blog"}
</Link>
<div className="mt-6 inline-flex rounded-full bg-brand-purple/10 px-4 py-2 text-sm font-semibold text-brand-purple">
{post.tag}
</div>
<h1 className="mt-6 text-4xl font-bold text-brand-dark">{post.title}</h1>
<div className="mt-4 text-sm text-gray-500">{post.date}</div>
<div className="mt-10 text-lg leading-8 text-gray-700">
<p>{post.body}</p>
</div>
</article>
</main>
);
}

49
app/[locale]/blog/page.js Normal file
View File

@@ -0,0 +1,49 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function BlogPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { blogPosts, navigation, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-brand-light text-gray-800">
<section className="mx-auto max-w-6xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{navigation.homeLabel}
</Link>
<h1 className="mt-4 text-4xl font-bold text-brand-dark">{navigation.blog}</h1>
<p className="mt-3 max-w-2xl text-gray-600">{shared.blogIntro}</p>
<div className="mt-12 grid gap-8 md:grid-cols-3">
{blogPosts.map((post) => (
<article key={post.slug} className="overflow-hidden rounded-2xl bg-white shadow-sm">
<div className="h-48 overflow-hidden">
<Image src={post.image} alt={post.title} width={800} height={600} className="h-full w-full object-cover transition duration-500 hover:scale-110" />
</div>
<div className="p-6">
<span className={`${post.tagClass} rounded px-2 py-1 text-xs font-bold`}>{post.tag}</span>
<h2 className="mt-4 text-xl font-bold">
<Link href={`/${locale}/blog/${post.slug}`} className="transition hover:text-brand-purple">
{post.title}
</Link>
</h2>
<p className="mt-3 text-sm text-gray-600">{post.excerpt}</p>
<div className="mt-4 text-sm text-gray-500">{post.date}</div>
</div>
</article>
))}
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,49 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function ContactPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { contact, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-brand-light text-gray-800">
<section className="mx-auto max-w-5xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{locale === "de" ? "Startseite" : "Home"}
</Link>
<div className="mt-6 grid gap-10 md:grid-cols-2">
<div>
<h1 className="text-4xl font-bold text-brand-dark">{shared.contactTitle}</h1>
<p className="mt-4 text-gray-600">{shared.contactIntro}</p>
<div className="mt-8 space-y-4 text-gray-700">
<p>{contact.location}</p>
<p>{contact.phone}</p>
<p>{contact.email}</p>
</div>
</div>
<form className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-gray-100">
<div className="grid gap-4">
<input className="rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.nameLabel} />
<input className="rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.emailLabel} />
<input className="rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.phoneLabel} />
<textarea className="min-h-36 rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.messageLabel} />
<button className="rounded-xl bg-brand-purple px-5 py-3 font-bold text-white transition hover:bg-purple-700">
{shared.sendMessage}
</button>
</div>
</form>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,40 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getCourseBySlug, isSupportedLocale, locales } from "@/lib/site-data";
const courseSlugs = ["beginner-a1", "intermediate-b1", "advanced-c1"];
export function generateStaticParams() {
return locales.flatMap((locale) => courseSlugs.map((slug) => ({ locale, slug })));
}
export default async function CourseDetailPage({ params }) {
const { locale, slug } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const course = getCourseBySlug(locale, slug);
if (!course) {
notFound();
}
return (
<main className="min-h-screen bg-white text-gray-800">
<div className="mx-auto max-w-4xl px-6 py-16">
<Link href={`/${locale}/courses`} className="text-sm font-semibold text-brand-purple">
{locale === "de" ? "Zurück zu Kursen" : "Back to courses"}
</Link>
<div className="mt-8 rounded-3xl bg-brand-light p-10">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-purple text-2xl font-bold text-white">{course.level}</div>
<h1 className="text-4xl font-bold text-brand-dark">{course.title}</h1>
<p className="mt-4 text-lg text-gray-600">{course.summary}</p>
<p className="mt-8 leading-8 text-gray-700">{course.body}</p>
<div className="mt-8 text-2xl font-black text-brand-dark">{course.price}</div>
</div>
</div>
</main>
);
}

View File

@@ -0,0 +1,48 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function CoursesPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { courses, navigation, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-brand-light text-gray-800">
<section className="border-b border-gray-200 bg-white">
<div className="mx-auto max-w-6xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{navigation.homeLabel}
</Link>
<h1 className="mt-4 text-4xl font-bold text-brand-dark">{navigation.courses}</h1>
<p className="mt-3 max-w-2xl text-gray-600">{shared.coursesIntro}</p>
</div>
</section>
<section className="mx-auto grid max-w-6xl gap-8 px-6 py-16 md:grid-cols-3">
{courses.map((course) => (
<article key={course.slug} className="rounded-3xl bg-white p-8 shadow-sm ring-1 ring-gray-100">
<div className="mb-6 flex h-16 w-16 items-center justify-center rounded-2xl bg-brand-purple/10 text-2xl font-bold text-brand-purple">
{course.level}
</div>
<h2 className="text-2xl font-bold">{course.title}</h2>
<p className="mt-3 text-gray-600">{course.summary}</p>
<div className="mt-6 text-2xl font-black text-brand-dark">
{course.price} <span className="text-sm font-normal text-gray-400">/ {shared.perLevel}</span>
</div>
<Link href={`/${locale}/courses/${course.slug}`} className="mt-6 inline-flex rounded-xl border-2 border-brand-dark px-5 py-3 font-bold transition hover:bg-brand-dark hover:text-white">
{shared.viewDetails}
</Link>
</article>
))}
</section>
</main>
);
}

33
app/[locale]/faq/page.js Normal file
View File

@@ -0,0 +1,33 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { FaqAccordion } from "@/components/site/faq-accordion";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function FaqPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { faqItems, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-white text-gray-800">
<section className="mx-auto max-w-4xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{locale === "de" ? "Startseite" : "Home"}
</Link>
<h1 className="mt-4 text-4xl font-bold text-brand-dark">{shared.faqTitle}</h1>
<p className="mt-3 max-w-2xl text-gray-600">{shared.faqIntro}</p>
<div className="mt-10">
<FaqAccordion items={faqItems} />
</div>
</section>
</main>
);
}

32
app/[locale]/page.js Normal file
View File

@@ -0,0 +1,32 @@
import { notFound } from "next/navigation";
import { LandingPage } from "@/components/site/landing-page";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export async function generateMetadata({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
return {};
}
const content = getLandingContent(locale);
return {
title: content.meta.title,
description: content.meta.description,
};
}
export default async function LocaleHomePage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
return <LandingPage locale={locale} content={getLandingContent(locale)} />;
}

View File

@@ -0,0 +1,48 @@
import Link from "next/link";
import { notFound } from "next/navigation";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function PlacementTestPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { courses, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-white text-gray-800">
<section className="mx-auto max-w-5xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{locale === "de" ? "Startseite" : "Home"}
</Link>
<div className="mt-6 grid gap-10 md:grid-cols-2">
<div>
<h1 className="text-4xl font-bold text-brand-dark">{shared.placementTitle}</h1>
<p className="mt-4 text-gray-600">{shared.placementIntro}</p>
</div>
<form className="rounded-3xl bg-brand-light p-8 shadow-sm ring-1 ring-gray-100">
<div className="grid gap-4">
<input className="rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.nameLabel} />
<input className="rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.emailLabel} />
<select className="rounded-xl border border-gray-200 px-4 py-3">
{courses.map((course) => (
<option key={course.slug}>{course.level}</option>
))}
</select>
<textarea className="min-h-36 rounded-xl border border-gray-200 px-4 py-3" placeholder={shared.messageLabel} />
<button className="rounded-xl bg-brand-purple px-5 py-3 font-bold text-white transition hover:bg-purple-700">
{shared.requestPlacement}
</button>
</div>
</form>
</div>
</section>
</main>
);
}

View File

@@ -0,0 +1,45 @@
import Image from "next/image";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getLandingContent, isSupportedLocale, locales } from "@/lib/site-data";
export function generateStaticParams() {
return locales.map((locale) => ({ locale }));
}
export default async function TeachersPage({ params }) {
const { locale } = await params;
if (!isSupportedLocale(locale)) {
notFound();
}
const { teachers, navigation, shared } = getLandingContent(locale);
return (
<main className="min-h-screen bg-brand-light text-gray-800">
<section className="mx-auto max-w-6xl px-6 py-16">
<Link href={`/${locale}`} className="text-sm font-semibold text-brand-purple">
{navigation.homeLabel}
</Link>
<h1 className="mt-4 text-4xl font-bold text-brand-dark">{navigation.teachers}</h1>
<p className="mt-3 max-w-2xl text-gray-600">{shared.teachersIntro}</p>
<div className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-4">
{teachers.map((teacher) => (
<article key={teacher.slug} className="overflow-hidden rounded-2xl bg-white shadow-sm">
<Image src={teacher.image} alt={teacher.name} width={600} height={700} className="h-64 w-full object-cover" />
<div className="p-6">
<div className="flex items-center justify-between gap-4">
<h2 className="text-xl font-bold">{teacher.name}</h2>
<span className="text-xl" aria-hidden="true">{teacher.flag}</span>
</div>
<p className="mt-3 text-sm font-semibold text-brand-purple">{teacher.title}</p>
<p className="mt-3 text-sm text-gray-500">{teacher.description}</p>
</div>
</article>
))}
</div>
</section>
</main>
);
}

58
app/globals.css Normal file
View File

@@ -0,0 +1,58 @@
@import "tailwindcss";
@import "@fontsource/poppins/300.css";
@import "@fontsource/poppins/400.css";
@import "@fontsource/poppins/600.css";
@import "@fontsource/poppins/700.css";
@import "@fontsource/poppins/800.css";
@theme {
--font-sans: "Poppins", sans-serif;
--color-brand-purple: #6d28d9;
--color-brand-yellow: #fbbf24;
--color-brand-dark: #111827;
--color-brand-light: #f3f4f6;
}
html {
scroll-behavior: smooth;
}
body {
margin: 0;
overflow-x: hidden;
background: #f3f4f6;
color: #1f2937;
font-family: "Poppins", sans-serif;
}
* {
box-sizing: border-box;
}
.gradient-text {
background: linear-gradient(to right, #6d28d9, #ec4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.blob-bg {
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path fill="%236D28D9" d="M44.7,-76.4C58.8,-69.2,71.8,-59.1,81.6,-46.3C91.4,-33.5,98,-18.1,98,-2.1C98,13.9,91.4,30.5,81.8,44.7C72.2,58.9,59.6,70.7,45.1,78.3C30.6,85.9,14.2,89.3,-1.7,92.2C-17.6,95.1,-35.2,97.5,-49.6,90C-64,82.5,-75.2,65.1,-83.4,47.1C-91.6,29.1,-96.8,10.5,-95,-7.4C-93.2,-25.3,-84.4,-42.5,-71.8,-55.4C-59.2,-68.3,-42.8,-76.9,-27.6,-79.8C-12.4,-82.7,1.6,-79.9,15.8,-75.7C30,-71.5,44.7,-65.9,44.7,-76.4Z" transform="translate(100 100) scale(1.1)" opacity="0.05"/></svg>');
background-position: top right;
background-repeat: no-repeat;
}
.section-reveal {
animation: fade-up 0.7s ease-out both;
}
@keyframes fade-up {
from {
opacity: 0;
transform: translateY(32px);
}
to {
opacity: 1;
transform: translateY(0);
}
}

20
app/layout.js Normal file
View File

@@ -0,0 +1,20 @@
import "./globals.css";
export const metadata = {
title: "DeutschAkademie Engel",
description: "Bilingual academy website built with Next.js.",
};
export default function RootLayout({ children }) {
return (
<html lang="en" className="scroll-smooth">
<head>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
</head>
<body>{children}</body>
</html>
);
}

15
app/not-found.js Normal file
View File

@@ -0,0 +1,15 @@
import Link from "next/link";
export default function NotFound() {
return (
<main className="flex min-h-screen items-center justify-center bg-brand-light px-6 text-center text-gray-800">
<div>
<p className="text-sm font-semibold uppercase tracking-[0.3em] text-brand-purple">404</p>
<h1 className="mt-4 text-4xl font-bold text-brand-dark">Page not found</h1>
<Link href="/en" className="mt-8 inline-flex rounded-full bg-brand-purple px-6 py-3 font-bold text-white">
Back home
</Link>
</div>
</main>
);
}

5
app/page.js Normal file
View File

@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default function RootPage() {
redirect("/en");
}

View File

@@ -0,0 +1,34 @@
"use client";
import { useState } from "react";
export function FaqAccordion({ items }) {
const [openIndex, setOpenIndex] = useState(items.findIndex((item) => item.open));
return (
<div className="space-y-4">
{items.map((item, index) => {
const isOpen = index === openIndex;
return (
<button
key={item.question}
type="button"
onClick={() => setOpenIndex(isOpen ? -1 : index)}
className={`block w-full rounded-xl p-6 text-left transition ${
isOpen
? "border border-brand-purple shadow-md"
: "cursor-pointer border border-gray-200 hover:border-brand-purple"
}`}
>
<span className={`flex items-center justify-between gap-6 text-lg font-bold ${isOpen ? "text-brand-purple" : ""}`}>
{item.question}
<i className={`fa-solid ${isOpen ? "fa-chevron-up" : "fa-chevron-down"} text-brand-purple`} />
</span>
<p className={`mt-4 text-gray-600 ${isOpen ? "" : "hidden"}`}>{item.answer}</p>
</button>
);
})}
</div>
);
}

77
components/site/header.js Normal file
View File

@@ -0,0 +1,77 @@
"use client";
import Link from "next/link";
import { useState } from "react";
export function SiteHeader({ locale, navigation, portalLabel }) {
const [open, setOpen] = useState(false);
const alternateLocale = locale === "en" ? "de" : "en";
return (
<header className="fixed top-0 z-50 w-full bg-white/95 shadow-sm backdrop-blur-md transition-all duration-300">
<div className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
<Link href={`/${locale}`} className="text-2xl font-extrabold tracking-tighter text-brand-dark">
DeutschAkademie <span className="text-brand-purple">Engel</span>
</Link>
<nav className="hidden items-center space-x-8 font-semibold lg:flex">
<Link href={`/${locale}/courses`} className="transition hover:text-brand-purple">
{navigation.courses}
</Link>
<Link href={`/${locale}/teachers`} className="transition hover:text-brand-purple">
{navigation.teachers}
</Link>
<Link href={`/${locale}/blog`} className="transition hover:text-brand-purple">
{navigation.blog}
</Link>
<div className="flex items-center space-x-2 border-l-2 pl-4">
<Link href="/en" className={locale === "en" ? "font-bold text-brand-purple" : "text-gray-500 transition hover:text-brand-dark"}>
EN
</Link>
<span className="text-gray-400">|</span>
<Link href="/de" className={locale === "de" ? "font-bold text-brand-purple" : "text-gray-500 transition hover:text-brand-dark"}>
DE
</Link>
</div>
<Link
href={`/${alternateLocale}`}
className="rounded-full bg-brand-purple px-6 py-2 text-white transition-all hover:-translate-y-0.5 hover:bg-purple-700 hover:shadow-lg"
>
{portalLabel}
</Link>
</nav>
<button
type="button"
className="text-2xl text-brand-dark lg:hidden"
aria-label="Open menu"
onClick={() => setOpen((value) => !value)}
>
<i className={`fa-solid ${open ? "fa-xmark" : "fa-bars"}`} />
</button>
</div>
{open ? (
<div className="border-t border-gray-100 bg-white px-6 py-5 lg:hidden">
<nav className="flex flex-col gap-4 font-semibold">
<Link href={`/${locale}/courses`} onClick={() => setOpen(false)}>
{navigation.courses}
</Link>
<Link href={`/${locale}/teachers`} onClick={() => setOpen(false)}>
{navigation.teachers}
</Link>
<Link href={`/${locale}/blog`} onClick={() => setOpen(false)}>
{navigation.blog}
</Link>
<Link href="/en" onClick={() => setOpen(false)}>
English
</Link>
<Link href="/de" onClick={() => setOpen(false)}>
Deutsch
</Link>
</nav>
</div>
) : null}
</header>
);
}

View File

@@ -0,0 +1,230 @@
import Image from "next/image";
import Link from "next/link";
import { FaqAccordion } from "@/components/site/faq-accordion";
import { SiteHeader } from "@/components/site/header";
function StarRow({ half = false }) {
return (
<div className="mb-4 flex text-brand-yellow">
<i className="fa-solid fa-star" />
<i className="fa-solid fa-star" />
<i className="fa-solid fa-star" />
<i className="fa-solid fa-star" />
<i className={half ? "fa-solid fa-star-half-stroke" : "fa-solid fa-star"} />
</div>
);
}
export function LandingPage({ locale, content }) {
const { navigation, hero, stats, courses, teachers, testimonials, blogPosts, faqItems, cta, footer, shared } = content;
return (
<main className="bg-brand-light font-sans text-gray-800">
<SiteHeader locale={locale} navigation={navigation} portalLabel={navigation.portal} />
<section className="blob-bg relative overflow-hidden pt-32 pb-20 lg:pt-48 lg:pb-32">
<div className="mx-auto flex max-w-6xl flex-col items-center px-6 lg:flex-row">
<div className="z-10 w-full text-center lg:w-1/2 lg:pr-12 lg:text-left section-reveal">
<div className="mb-6 inline-block animate-bounce rounded-full bg-brand-yellow px-4 py-1 font-bold text-brand-dark">{hero.badge}</div>
<h1 className="mb-6 text-5xl leading-tight font-extrabold lg:text-7xl">
{hero.titleStart} <br />
<span className="gradient-text">{hero.titleAccent}</span>
</h1>
<p className="mb-8 text-lg font-light text-gray-600">{hero.description}</p>
<div className="flex flex-col justify-center space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4 lg:justify-start">
<Link href={`/${locale}/placement-test`} className="rounded-full bg-brand-purple px-8 py-4 text-center font-bold text-white shadow-xl transition-all hover:-translate-y-1 hover:bg-purple-700">
{hero.primaryCta}
</Link>
<Link href={`/${locale}/courses`} className="rounded-full border-2 border-gray-200 bg-white px-8 py-4 text-center font-bold text-brand-dark transition-all hover:border-brand-purple hover:text-brand-purple">
{hero.secondaryCta}
</Link>
</div>
</div>
<div className="relative z-10 mt-16 w-full lg:mt-0 lg:w-1/2 section-reveal">
<Image src="/images/hero-students.jpg" alt="Students" width={1200} height={1000} priority className="h-[500px] w-full rounded-3xl border-8 border-white object-cover shadow-2xl" />
</div>
</div>
</section>
<section className="bg-brand-dark py-12 text-white">
<div className="mx-auto grid max-w-6xl grid-cols-2 gap-8 divide-x divide-gray-700 px-6 text-center md:grid-cols-4">
{stats.map((stat) => (
<div key={stat.label} className="section-reveal">
<h3 className="mb-2 text-4xl font-black text-brand-yellow">{stat.value}</h3>
<p className="font-medium text-gray-400">{stat.label}</p>
</div>
))}
</div>
</section>
<section id="courses" className="bg-white py-24">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-16 text-center section-reveal">
<h2 className="mb-4 text-4xl font-bold text-brand-dark">{shared.coursesTitle}</h2>
<p className="mx-auto max-w-2xl text-gray-600">{shared.coursesIntro}</p>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{courses.map((course, index) => (
<div key={course.slug} className={`relative rounded-3xl p-8 transition-all ${index === 1 ? "transform bg-brand-purple text-white shadow-2xl shadow-purple-500/20 md:-translate-y-4" : "group border border-gray-100 bg-gray-50 hover:border-brand-purple hover:shadow-xl"}`}>
{index === 1 ? <div className="absolute top-0 right-0 rounded-bl-xl rounded-tr-3xl bg-brand-yellow px-3 py-1 text-xs font-bold text-brand-dark">{shared.popular}</div> : null}
<div className={`mb-6 flex h-16 w-16 items-center justify-center rounded-2xl text-2xl font-bold ${index === 1 ? "bg-white/20" : "bg-brand-purple/10 text-brand-purple"}`}>{course.level}</div>
<h3 className="mb-3 text-2xl font-bold">{course.title}</h3>
<p className={index === 1 ? "mb-6 text-purple-100" : "mb-6 text-gray-600"}>{course.summary}</p>
<div className="mb-6 text-2xl font-black">
{course.price} <span className={index === 1 ? "text-sm font-normal text-purple-200" : "text-sm font-normal text-gray-400"}>/ {shared.perLevel}</span>
</div>
<Link href={`/${locale}/courses/${course.slug}`} className={`block w-full rounded-xl py-3 text-center font-bold transition-colors ${index === 1 ? "bg-white text-brand-purple hover:bg-gray-100" : "border-2 border-brand-dark hover:bg-brand-dark hover:text-white"}`}>
{shared.selectCourse}
</Link>
</div>
))}
</div>
</div>
</section>
<section id="teachers" className="bg-brand-light py-24">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-12 flex items-end justify-between gap-6 section-reveal">
<div>
<h2 className="mb-4 text-4xl font-bold text-brand-dark">{shared.teachersTitle}</h2>
<p className="text-gray-600">{shared.teachersIntro}</p>
</div>
<Link href={`/${locale}/teachers`} className="hidden font-bold text-brand-purple hover:underline md:block">
{shared.viewAllTeachers}
</Link>
</div>
<div className="grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4">
{teachers.map((teacher) => (
<div key={teacher.slug} className="overflow-hidden rounded-2xl bg-white shadow-sm transition-all hover:shadow-xl">
<Image src={teacher.image} alt={teacher.name} width={600} height={800} className="h-64 w-full object-cover" />
<div className="p-6">
<div className="mb-2 flex items-center justify-between">
<h4 className="text-xl font-bold">{teacher.name}</h4>
<span className="text-xl" title={teacher.country}>{teacher.flag}</span>
</div>
<p className="mb-3 text-sm font-semibold text-brand-purple">{teacher.title}</p>
<p className="text-sm text-gray-500">{teacher.description}</p>
</div>
</div>
))}
</div>
</div>
</section>
<section className="overflow-hidden bg-white py-24">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-16 text-center section-reveal">
<h2 className="mb-4 text-4xl font-bold text-brand-dark">{shared.testimonialsTitle}</h2>
<p className="text-gray-600">{shared.testimonialsIntro}</p>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{testimonials.map((item) => (
<div key={item.name} className="relative rounded-3xl bg-gray-50 p-8">
<i className="fa-solid fa-quote-left absolute top-6 right-8 text-4xl text-brand-yellow/30" />
<StarRow half={item.stars === "half"} />
<p className="mb-6 italic text-gray-600">{item.text}</p>
<div className="flex items-center">
<div className="mr-4 h-12 w-12 overflow-hidden rounded-full bg-gray-300">
<Image src={item.image} alt={item.name} width={150} height={150} className="h-full w-full object-cover" />
</div>
<div>
<h5 className="font-bold">{item.name}</h5>
<span className="text-sm text-gray-500">{item.role}</span>
</div>
</div>
</div>
))}
</div>
</div>
</section>
<section id="blog" className="bg-brand-light py-24">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-12 flex items-end justify-between gap-6 section-reveal">
<div>
<h2 className="mb-4 text-4xl font-bold text-brand-dark">{shared.blogTitle}</h2>
<p className="text-gray-600">{shared.blogIntro}</p>
</div>
<Link href={`/${locale}/blog`} className="hidden font-bold text-brand-purple hover:underline md:block">
{shared.readAllArticles}
</Link>
</div>
<div className="grid grid-cols-1 gap-8 md:grid-cols-3">
{blogPosts.map((post) => (
<article key={post.slug} className="group overflow-hidden rounded-2xl bg-white shadow-sm transition-all hover:shadow-xl">
<div className="h-48 overflow-hidden">
<Image src={post.image} alt={post.title} width={800} height={600} className="h-full w-full object-cover transition duration-500 group-hover:scale-110" />
</div>
<div className="p-6">
<div className="mb-3 flex space-x-2">
<span className={`${post.tagClass} rounded px-2 py-1 text-xs font-bold`}>{post.tag}</span>
</div>
<h3 className="mb-3 text-xl font-bold transition hover:text-brand-purple">
<Link href={`/${locale}/blog/${post.slug}`}>{post.title}</Link>
</h3>
<p className="mb-4 text-sm text-gray-600">{post.excerpt}</p>
<div className="text-sm text-gray-500"><i className="fa-regular fa-calendar mr-2" />{post.date}</div>
</div>
</article>
))}
</div>
</div>
</section>
<section className="bg-white py-24">
<div className="mx-auto max-w-3xl px-6">
<div className="mb-12 text-center section-reveal">
<h2 className="mb-4 text-4xl font-bold text-brand-dark">{shared.faqTitle}</h2>
</div>
<FaqAccordion items={faqItems} />
</div>
</section>
<section id="register" className="relative overflow-hidden bg-brand-purple py-20">
<div className="absolute top-0 left-0 h-64 w-64 -translate-x-1/2 -translate-y-1/2 rounded-full bg-white opacity-5" />
<div className="absolute right-0 bottom-0 h-96 w-96 translate-x-1/3 translate-y-1/3 rounded-full bg-brand-yellow opacity-10" />
<div className="relative z-10 mx-auto max-w-6xl px-6 text-center section-reveal">
<h2 className="mb-6 text-4xl font-extrabold text-white md:text-5xl">{cta.title}</h2>
<p className="mx-auto mb-8 max-w-2xl text-lg text-purple-100">{cta.description}</p>
<Link href={`/${locale}/contact`} className="inline-flex rounded-full bg-brand-yellow px-10 py-4 text-lg font-black text-brand-dark shadow-2xl transition-all hover:scale-105 hover:bg-white">
{cta.button}
</Link>
</div>
</section>
<footer className="bg-brand-dark pt-20 pb-10 text-white">
<div className="mx-auto max-w-6xl px-6">
<div className="mb-16 grid grid-cols-1 gap-12 md:grid-cols-4">
<div className="md:col-span-2">
<h4 className="mb-6 text-2xl font-extrabold">DeutschAkademie <span className="text-brand-purple">Engel</span></h4>
<p className="mb-6 max-w-sm text-gray-400">{footer.description}</p>
<div className="flex space-x-4">
{footer.socials.map((social) => (
<a key={social.label} href={social.href} className="flex h-10 w-10 items-center justify-center rounded-full bg-gray-800 transition hover:bg-brand-purple" aria-label={social.label}>
<i className={social.icon} />
</a>
))}
</div>
</div>
<div>
<h4 className="mb-6 text-lg font-bold">{footer.exploreTitle}</h4>
<ul className="space-y-3 text-gray-400">
{footer.links.map((link) => (
<li key={link.label}>
<Link href={link.href.replace("{locale}", locale)} className="transition hover:text-white">{link.label}</Link>
</li>
))}
</ul>
</div>
<div>
<h4 className="mb-6 text-lg font-bold">{footer.contactTitle}</h4>
<ul className="space-y-3 text-gray-400">
<li><i className="fa-solid fa-location-dot w-6" /> {footer.location}</li>
<li><i className="fa-solid fa-phone w-6" /> {footer.phone}</li>
<li><i className="fa-solid fa-envelope w-6" /> {footer.email}</li>
</ul>
</div>
</div>
<div className="flex flex-col items-center justify-between border-t border-gray-800 pt-8 text-center text-sm text-gray-500 md:flex-row">
<p>{footer.copyright}</p>
<div className="mt-4 space-x-4 md:mt-0">
<Link href={`/${locale}/contact`} className="transition hover:text-white">{footer.privacyLabel}</Link>
<Link href={`/${locale}/contact`} className="transition hover:text-white">{footer.termsLabel}</Link>
</div>
</div>
</div>
</footer>
</main>
);
}

431
index.html Normal file
View File

@@ -0,0 +1,431 @@
<!DOCTYPE html>
<html lang="en" class="scroll-smooth">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>DeutschAkademie Engel | Learn German in Austria</title>
<script src="https://cdn.tailwindcss.com"></script>
<link href="https://unpkg.com/aos@2.3.1/dist/aos.css" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600;700;800&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Poppins', 'sans-serif'] },
colors: {
brand: {
purple: '#6D28D9',
yellow: '#FBBF24',
dark: '#111827',
light: '#F3F4F6'
}
}
}
}
}
</script>
<style>
.gradient-text {
background: linear-gradient(to right, #6D28D9, #EC4899);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.blob-bg {
background-image: url('data:image/svg+xml;utf8,<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg"><path fill="%236D28D9" d="M44.7,-76.4C58.8,-69.2,71.8,-59.1,81.6,-46.3C91.4,-33.5,98,-18.1,98,-2.1C98,13.9,91.4,30.5,81.8,44.7C72.2,58.9,59.6,70.7,45.1,78.3C30.6,85.9,14.2,89.3,-1.7,92.2C-17.6,95.1,-35.2,97.5,-49.6,90C-64,82.5,-75.2,65.1,-83.4,47.1C-91.6,29.1,-96.8,10.5,-95,-7.4C-93.2,-25.3,-84.4,-42.5,-71.8,-55.4C-59.2,-68.3,-42.8,-76.9,-27.6,-79.8C-12.4,-82.7,1.6,-79.9,15.8,-75.7C30,-71.5,44.7,-65.9,44.7,-76.4Z" transform="translate(100 100) scale(1.1)" opacity="0.05"/></svg>');
background-repeat: no-repeat;
background-position: top right;
}
</style>
</head>
<body class="bg-brand-light font-sans text-gray-800 overflow-x-hidden">
<header class="fixed w-full top-0 z-50 bg-white/95 backdrop-blur-md shadow-sm transition-all duration-300">
<div class="container mx-auto px-6 py-4 flex justify-between items-center">
<a href="#" class="text-2xl font-extrabold tracking-tighter text-brand-dark">
DeutschAkademie <span class="text-brand-purple">Engel</span>
</a>
<nav class="hidden lg:flex space-x-8 items-center font-semibold">
<a href="#courses" class="hover:text-brand-purple transition">Courses</a>
<a href="#teachers" class="hover:text-brand-purple transition">Teachers</a>
<a href="#blog" class="hover:text-brand-purple transition">Blog</a>
<div class="flex items-center space-x-2 border-l-2 pl-4">
<span class="text-brand-purple font-bold cursor-pointer">EN</span>
<span class="text-gray-400">|</span>
<span class="text-gray-500 hover:text-brand-dark cursor-pointer transition">DE</span>
</div>
<a href="#register" class="bg-brand-purple text-white px-6 py-2 rounded-full hover:bg-purple-700 hover:shadow-lg transform hover:-translate-y-0.5 transition-all">
Portal
</a>
</nav>
<button class="lg:hidden text-2xl text-brand-dark"><i class="fa-solid fa-bars"></i></button>
</div>
</header>
<section class="relative pt-32 pb-20 lg:pt-48 lg:pb-32 overflow-hidden blob-bg">
<div class="container mx-auto px-6 flex flex-col lg:flex-row items-center">
<div class="w-full lg:w-1/2 lg:pr-12 text-center lg:text-left z-10" data-aos="fade-right">
<div class="inline-block bg-brand-yellow text-brand-dark font-bold px-4 py-1 rounded-full mb-6 animate-bounce">
🚀 Start Your Journey Today
</div>
<h1 class="text-5xl lg:text-7xl font-extrabold leading-tight mb-6">
Speak German <br>
<span class="gradient-text">With Confidence.</span>
</h1>
<p class="text-lg text-gray-600 mb-8 font-light">
Join Austria's most energetic language institute. Master German with native experts, modern methods, and a supportive community.
</p>
<div class="flex flex-col sm:flex-row justify-center lg:justify-start space-y-4 sm:space-y-0 sm:space-x-4">
<button class="bg-brand-purple text-white font-bold px-8 py-4 rounded-full shadow-xl hover:bg-purple-700 transform hover:-translate-y-1 transition-all">
Take Placement Test
</button>
<button class="bg-white text-brand-dark font-bold border-2 border-gray-200 px-8 py-4 rounded-full hover:border-brand-purple hover:text-brand-purple transition-all">
View Courses
</button>
</div>
</div>
<div class="w-full lg:w-1/2 mt-16 lg:mt-0 relative z-10" data-aos="fade-left" data-aos-delay="200">
<img src="https://images.unsplash.com/photo-1523240795612-9a054b0db644?ixlib=rb-4.0.3&auto=format&fit=crop&w=800&q=80" alt="Students" class="rounded-3xl shadow-2xl border-8 border-white object-cover h-[500px] w-full">
</div>
</div>
</section>
<section class="bg-brand-dark text-white py-12">
<div class="container mx-auto px-6 grid grid-cols-2 md:grid-cols-4 gap-8 text-center divide-x divide-gray-700">
<div data-aos="zoom-in"><h3 class="text-4xl font-black text-brand-yellow mb-2">50+</h3><p class="text-gray-400 font-medium">Native Teachers</p></div>
<div data-aos="zoom-in" data-aos-delay="100"><h3 class="text-4xl font-black text-brand-yellow mb-2">A1-C2</h3><p class="text-gray-400 font-medium">Full Curriculum</p></div>
<div data-aos="zoom-in" data-aos-delay="200"><h3 class="text-4xl font-black text-brand-yellow mb-2">10k+</h3><p class="text-gray-400 font-medium">Happy Students</p></div>
<div data-aos="zoom-in" data-aos-delay="300"><h3 class="text-4xl font-black text-brand-yellow mb-2">4.9/5</h3><p class="text-gray-400 font-medium">Average Rating</p></div>
</div>
</section>
<section id="courses" class="py-24 bg-white">
<div class="container mx-auto px-6">
<div class="text-center mb-16" data-aos="fade-up">
<h2 class="text-4xl font-bold text-brand-dark mb-4">Choose Your Level</h2>
<p class="text-gray-600 max-w-2xl mx-auto">From complete beginners to advanced speakers, we have the right course for you.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-gray-50 rounded-3xl p-8 border border-gray-100 hover:shadow-xl hover:border-brand-purple transition-all group" data-aos="fade-up">
<div class="w-16 h-16 bg-brand-purple/10 text-brand-purple rounded-2xl flex items-center justify-center text-2xl font-bold mb-6">A1</div>
<h3 class="text-2xl font-bold mb-3">Beginner</h3>
<p class="text-gray-600 mb-6">Learn to introduce yourself and navigate daily life.</p>
<div class="text-2xl font-black text-brand-dark mb-6">€199 <span class="text-sm text-gray-400 font-normal">/ level</span></div>
<button class="w-full py-3 rounded-xl border-2 border-brand-dark font-bold hover:bg-brand-dark hover:text-white transition-colors">Select Course</button>
</div>
<div class="bg-brand-purple rounded-3xl p-8 text-white shadow-2xl shadow-purple-500/20 transform md:-translate-y-4 relative" data-aos="fade-up" data-aos-delay="100">
<div class="absolute top-0 right-0 bg-brand-yellow text-brand-dark text-xs font-bold px-3 py-1 rounded-bl-xl rounded-tr-3xl">POPULAR</div>
<div class="w-16 h-16 bg-white/20 rounded-2xl flex items-center justify-center text-2xl font-bold mb-6">B1</div>
<h3 class="text-2xl font-bold mb-3">Intermediate</h3>
<p class="text-purple-100 mb-6">Master complex conversations and prepare for work.</p>
<div class="text-2xl font-black mb-6">€249 <span class="text-sm text-purple-200 font-normal">/ level</span></div>
<button class="w-full py-3 rounded-xl bg-white text-brand-purple font-bold hover:bg-gray-100 transition-colors">Select Course</button>
</div>
<div class="bg-gray-50 rounded-3xl p-8 border border-gray-100 hover:shadow-xl hover:border-brand-purple transition-all group" data-aos="fade-up" data-aos-delay="200">
<div class="w-16 h-16 bg-brand-purple/10 text-brand-purple rounded-2xl flex items-center justify-center text-2xl font-bold mb-6">C1</div>
<h3 class="text-2xl font-bold mb-3">Advanced</h3>
<p class="text-gray-600 mb-6">Speak fluently in academic and business environments.</p>
<div class="text-2xl font-black text-brand-dark mb-6">€299 <span class="text-sm text-gray-400 font-normal">/ level</span></div>
<button class="w-full py-3 rounded-xl border-2 border-brand-dark font-bold hover:bg-brand-dark hover:text-white transition-colors">Select Course</button>
</div>
</div>
</div>
</section>
<section id="teachers" class="py-24 bg-brand-light">
<div class="container mx-auto px-6">
<div class="flex justify-between items-end mb-12" data-aos="fade-up">
<div>
<h2 class="text-4xl font-bold text-brand-dark mb-4">Meet Our Native Experts</h2>
<p class="text-gray-600">Learn from passionate, certified instructors from Austria and Germany.</p>
</div>
<a href="#" class="hidden md:block text-brand-purple font-bold hover:underline">View all teachers &rarr;</a>
</div>
<div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-8">
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all" data-aos="fade-up">
<img src="https://images.unsplash.com/photo-1573496359142-b8d87734a5a2?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Teacher" class="w-full h-64 object-cover">
<div class="p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="text-xl font-bold">Sarah Müller</h4>
<span class="text-xl" title="Austria">🇦🇹</span>
</div>
<p class="text-sm text-brand-purple font-semibold mb-3">Goethe Examiner</p>
<p class="text-gray-500 text-sm">Specializes in B2-C1 exam preparation with 10 years of experience.</p>
</div>
</div>
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all" data-aos="fade-up" data-aos-delay="100">
<img src="https://images.unsplash.com/photo-1560250097-0b93528c311a?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Teacher" class="w-full h-64 object-cover">
<div class="p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="text-xl font-bold">Lukas Weber</h4>
<span class="text-xl" title="Germany">🇩🇪</span>
</div>
<p class="text-sm text-brand-purple font-semibold mb-3">A1-A2 Specialist</p>
<p class="text-gray-500 text-sm">Makes learning grammar fun and engaging for absolute beginners.</p>
</div>
</div>
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all" data-aos="fade-up" data-aos-delay="200">
<img src="https://images.unsplash.com/photo-1580489944761-15a19d654956?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Teacher" class="w-full h-64 object-cover">
<div class="p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="text-xl font-bold">Anna Schmidt</h4>
<span class="text-xl" title="Austria">🇦🇹</span>
</div>
<p class="text-sm text-brand-purple font-semibold mb-3">Business German</p>
<p class="text-gray-500 text-sm">Helps professionals integrate into the DACH corporate world.</p>
</div>
</div>
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all" data-aos="fade-up" data-aos-delay="300">
<img src="https://images.unsplash.com/photo-1519085360753-af0119f7cbe7?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Teacher" class="w-full h-64 object-cover">
<div class="p-6">
<div class="flex justify-between items-center mb-2">
<h4 class="text-xl font-bold">Felix Bauer</h4>
<span class="text-xl" title="Germany">🇩🇪</span>
</div>
<p class="text-sm text-brand-purple font-semibold mb-3">Conversation Coach</p>
<p class="text-gray-500 text-sm">Focuses on pronunciation and speaking confidence.</p>
</div>
</div>
</div>
</div>
</section>
<section class="py-24 bg-white overflow-hidden">
<div class="container mx-auto px-6">
<div class="text-center mb-16" data-aos="fade-up">
<h2 class="text-4xl font-bold text-brand-dark mb-4">Student Success Stories</h2>
<p class="text-gray-600">Don't just take our word for it. Hear from our community.</p>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-gray-50 p-8 rounded-3xl relative" data-aos="fade-up">
<i class="fa-solid fa-quote-left text-4xl text-brand-yellow/30 absolute top-6 right-8"></i>
<div class="flex text-brand-yellow mb-4">
<i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i>
</div>
<p class="text-gray-600 mb-6 italic">"I passed my ÖSD B2 exam on the first try! The teachers at Engel are incredibly supportive and the materials are top-notch."</p>
<div class="flex items-center">
<div class="w-12 h-12 bg-gray-300 rounded-full mr-4 overflow-hidden"><img src="https://i.pravatar.cc/150?img=32" alt="User"></div>
<div>
<h5 class="font-bold">Elena R.</h5>
<span class="text-sm text-gray-500">Student Visa Applicant</span>
</div>
</div>
</div>
<div class="bg-gray-50 p-8 rounded-3xl relative" data-aos="fade-up" data-aos-delay="100">
<i class="fa-solid fa-quote-left text-4xl text-brand-yellow/30 absolute top-6 right-8"></i>
<div class="flex text-brand-yellow mb-4">
<i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i>
</div>
<p class="text-gray-600 mb-6 italic">"The placement test was very accurate. I started at A2.2 and the pacing of the classes was perfect for someone working full-time."</p>
<div class="flex items-center">
<div class="w-12 h-12 bg-gray-300 rounded-full mr-4 overflow-hidden"><img src="https://i.pravatar.cc/150?img=11" alt="User"></div>
<div>
<h5 class="font-bold">Mark T.</h5>
<span class="text-sm text-gray-500">IT Professional</span>
</div>
</div>
</div>
<div class="bg-gray-50 p-8 rounded-3xl relative" data-aos="fade-up" data-aos-delay="200">
<i class="fa-solid fa-quote-left text-4xl text-brand-yellow/30 absolute top-6 right-8"></i>
<div class="flex text-brand-yellow mb-4">
<i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star"></i><i class="fa-solid fa-star-half-stroke"></i>
</div>
<p class="text-gray-600 mb-6 italic">"Love the energy in the online classes. It doesn't feel like a boring school. It feels like hanging out with friends while learning German."</p>
<div class="flex items-center">
<div class="w-12 h-12 bg-gray-300 rounded-full mr-4 overflow-hidden"><img src="https://i.pravatar.cc/150?img=5" alt="User"></div>
<div>
<h5 class="font-bold">Sarah J.</h5>
<span class="text-sm text-gray-500">University Student</span>
</div>
</div>
</div>
</div>
</div>
</section>
<section id="blog" class="py-24 bg-brand-light">
<div class="container mx-auto px-6">
<div class="flex justify-between items-end mb-12" data-aos="fade-up">
<div>
<h2 class="text-4xl font-bold text-brand-dark mb-4">Latest Tips & News</h2>
<p class="text-gray-600">Resources to help you master the German language faster.</p>
</div>
<a href="#" class="hidden md:block text-brand-purple font-bold hover:underline">Read all articles &rarr;</a>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-8">
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all group" data-aos="fade-up">
<div class="overflow-hidden h-48">
<img src="https://images.unsplash.com/photo-1456513080510-7bf3a84b82f8?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Blog" class="w-full h-full object-cover group-hover:scale-110 transition duration-500">
</div>
<div class="p-6">
<div class="flex space-x-2 mb-3">
<span class="bg-purple-100 text-brand-purple text-xs font-bold px-2 py-1 rounded">Study Tips</span>
</div>
<h3 class="text-xl font-bold mb-3 hover:text-brand-purple transition"><a href="#">5 Hacks to Remember German Articles (Der, Die, Das)</a></h3>
<p class="text-gray-600 text-sm mb-4">Struggling with noun genders? These simple tricks will save you hours of memorization.</p>
<div class="text-sm text-gray-500"><i class="fa-regular fa-calendar mr-2"></i> Oct 12, 2026</div>
</div>
</div>
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all group" data-aos="fade-up" data-aos-delay="100">
<div class="overflow-hidden h-48">
<img src="https://images.unsplash.com/photo-1516550893923-42d28e5677af?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Blog" class="w-full h-full object-cover group-hover:scale-110 transition duration-500">
</div>
<div class="p-6">
<div class="flex space-x-2 mb-3">
<span class="bg-yellow-100 text-yellow-800 text-xs font-bold px-2 py-1 rounded">Life in Austria</span>
</div>
<h3 class="text-xl font-bold mb-3 hover:text-brand-purple transition"><a href="#">How to Register Your Address (Meldezettel) in Vienna</a></h3>
<p class="text-gray-600 text-sm mb-4">A step-by-step guide to navigating Austrian bureaucracy as a new student or expat.</p>
<div class="text-sm text-gray-500"><i class="fa-regular fa-calendar mr-2"></i> Oct 05, 2026</div>
</div>
</div>
<div class="bg-white rounded-2xl overflow-hidden shadow-sm hover:shadow-xl transition-all group" data-aos="fade-up" data-aos-delay="200">
<div class="overflow-hidden h-48">
<img src="https://images.unsplash.com/photo-1434030216411-0b793f4b4173?ixlib=rb-4.0.3&auto=format&fit=crop&w=600&q=80" alt="Blog" class="w-full h-full object-cover group-hover:scale-110 transition duration-500">
</div>
<div class="p-6">
<div class="flex space-x-2 mb-3">
<span class="bg-green-100 text-green-800 text-xs font-bold px-2 py-1 rounded">Exams</span>
</div>
<h3 class="text-xl font-bold mb-3 hover:text-brand-purple transition"><a href="#">ÖSD vs. Goethe: Which German Exam Should You Take?</a></h3>
<p class="text-gray-600 text-sm mb-4">Comparing the two most popular German proficiency certificates and helping you choose.</p>
<div class="text-sm text-gray-500"><i class="fa-regular fa-calendar mr-2"></i> Sep 28, 2026</div>
</div>
</div>
</div>
</div>
</section>
<section class="py-24 bg-white">
<div class="container mx-auto px-6 max-w-3xl">
<div class="text-center mb-12" data-aos="fade-up">
<h2 class="text-4xl font-bold text-brand-dark mb-4">Frequently Asked Questions</h2>
</div>
<div class="space-y-4" data-aos="fade-up">
<div class="border border-gray-200 rounded-xl p-6 hover:border-brand-purple transition cursor-pointer">
<h3 class="font-bold text-lg flex justify-between items-center">
How do I know which level to choose?
<i class="fa-solid fa-chevron-down text-brand-purple"></i>
</h3>
<p class="text-gray-600 mt-4 hidden">We offer a free, 10-minute online placement test. Once completed, our system will automatically recommend the best course for your current proficiency level.</p>
</div>
<div class="border border-brand-purple rounded-xl p-6 shadow-md">
<h3 class="font-bold text-lg flex justify-between items-center text-brand-purple">
Are the classes online or in-person?
<i class="fa-solid fa-chevron-up text-brand-purple"></i>
</h3>
<p class="text-gray-600 mt-4">We offer both! You can join our vibrant campus in Vienna, or participate in our highly interactive live online classes from anywhere in the world.</p>
</div>
<div class="border border-gray-200 rounded-xl p-6 hover:border-brand-purple transition cursor-pointer">
<h3 class="font-bold text-lg flex justify-between items-center">
Do you provide certificates after completion?
<i class="fa-solid fa-chevron-down text-brand-purple"></i>
</h3>
<p class="text-gray-600 mt-4 hidden">Yes, you will receive a certificate of attendance. We also prepare you specifically for official ÖSD and Goethe exams.</p>
</div>
</div>
</div>
</section>
<section class="bg-brand-purple py-20 relative overflow-hidden">
<div class="absolute top-0 left-0 w-64 h-64 bg-white opacity-5 rounded-full -translate-x-1/2 -translate-y-1/2"></div>
<div class="absolute bottom-0 right-0 w-96 h-96 bg-brand-yellow opacity-10 rounded-full translate-x-1/3 translate-y-1/3"></div>
<div class="container mx-auto px-6 relative z-10 text-center" data-aos="zoom-in">
<h2 class="text-4xl md:text-5xl font-extrabold text-white mb-6">Ready to speak German fluently?</h2>
<p class="text-purple-100 text-lg mb-8 max-w-2xl mx-auto">Join thousands of successful students. Register today and get access to our exclusive learning portal.</p>
<button class="bg-brand-yellow text-brand-dark font-black px-10 py-4 rounded-full shadow-2xl hover:bg-white transform hover:scale-105 transition-all text-lg">
Create Your Free Account
</button>
</div>
</section>
<footer class="bg-brand-dark text-white pt-20 pb-10">
<div class="container mx-auto px-6">
<div class="grid grid-cols-1 md:grid-cols-4 gap-12 mb-16">
<div class="col-span-1 md:col-span-2">
<h4 class="text-2xl font-extrabold mb-6">DeutschAkademie <span class="text-brand-purple">Engel</span></h4>
<p class="text-gray-400 mb-6 max-w-sm">Making German learning accessible, energetic, and highly effective in the heart of Austria.</p>
<div class="flex space-x-4">
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-brand-purple transition"><i class="fa-brands fa-instagram"></i></a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-brand-purple transition"><i class="fa-brands fa-linkedin-in"></i></a>
<a href="#" class="w-10 h-10 bg-gray-800 rounded-full flex items-center justify-center hover:bg-brand-purple transition"><i class="fa-brands fa-youtube"></i></a>
</div>
</div>
<div>
<h4 class="text-lg font-bold mb-6">Explore</h4>
<ul class="space-y-3 text-gray-400">
<li><a href="#" class="hover:text-white transition">All Courses</a></li>
<li><a href="#" class="hover:text-white transition">Placement Test</a></li>
<li><a href="#" class="hover:text-white transition">Student Portal</a></li>
<li><a href="#" class="hover:text-white transition">Blog & Tips</a></li>
</ul>
</div>
<div>
<h4 class="text-lg font-bold mb-6">Contact</h4>
<ul class="space-y-3 text-gray-400">
<li><i class="fa-solid fa-location-dot w-6"></i> Vienna, Austria</li>
<li><i class="fa-solid fa-phone w-6"></i> +43 1 234 5678</li>
<li><i class="fa-solid fa-envelope w-6"></i> info@engel-akademie.at</li>
</ul>
</div>
</div>
<div class="border-t border-gray-800 pt-8 text-center text-gray-500 text-sm flex flex-col md:flex-row justify-between items-center">
<p>&copy; 2026 DeutschAkademie Engel. All rights reserved.</p>
<div class="space-x-4 mt-4 md:mt-0">
<a href="#" class="hover:text-white transition">Privacy Policy</a>
<a href="#" class="hover:text-white transition">Terms of Service</a>
</div>
</div>
</div>
</footer>
<script src="https://unpkg.com/aos@2.3.1/dist/aos.js"></script>
<script>
AOS.init({
once: true,
offset: 100,
duration: 800
});
// Simple script to handle FAQ accordion logic
document.querySelectorAll('.border-gray-200, .border-brand-purple').forEach(item => {
item.addEventListener('click', () => {
const p = item.querySelector('p');
const icon = item.querySelector('i');
const h3 = item.querySelector('h3');
if(p.classList.contains('hidden')) {
p.classList.remove('hidden');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
item.classList.add('border-brand-purple', 'shadow-md');
item.classList.remove('border-gray-200');
h3.classList.add('text-brand-purple');
} else {
p.classList.add('hidden');
icon.classList.add('fa-chevron-down');
icon.classList.remove('fa-chevron-up');
item.classList.remove('border-brand-purple', 'shadow-md');
item.classList.add('border-gray-200');
h3.classList.remove('text-brand-purple');
}
});
});
</script>
</body>
</html>

8
jsconfig.json Normal file
View File

@@ -0,0 +1,8 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
}
}
}

284
lib/site-data.js Normal file
View File

@@ -0,0 +1,284 @@
export const locales = ["en", "de"];
const images = {
hero: "/images/hero-students.jpg",
teachers: {
sarah: "/images/teacher-sarah.jpg",
lukas: "/images/teacher-lukas.jpg",
anna: "/images/teacher-anna.jpg",
felix: "/images/teacher-felix.jpg",
},
students: {
elena: "/images/student-elena.jpg",
mark: "/images/student-mark.jpg",
sarah: "/images/student-sarah.jpg",
},
posts: {
articles: "/images/blog-articles.jpg",
austria: "/images/blog-austria.jpg",
exams: "/images/blog-exams.jpg",
},
};
const english = {
meta: {
title: "DeutschAkademie Engel | Learn German in Austria",
description: "Join Austria's most energetic language institute and learn German with confidence.",
},
navigation: {
homeLabel: "Home",
courses: "Courses",
teachers: "Teachers",
blog: "Blog",
portal: "Portal",
},
hero: {
badge: "Start Your Journey Today",
titleStart: "Speak German",
titleAccent: "With Confidence.",
description:
"Join Austria's most energetic language institute. Master German with native experts, modern methods, and a supportive community.",
primaryCta: "Take Placement Test",
secondaryCta: "View Courses",
},
stats: [
{ value: "50+", label: "Native Teachers" },
{ value: "A1-C2", label: "Full Curriculum" },
{ value: "10k+", label: "Happy Students" },
{ value: "4.9/5", label: "Average Rating" },
],
courses: [
{
slug: "beginner-a1",
level: "A1",
title: "Beginner",
summary: "Learn to introduce yourself and navigate daily life.",
body: "This level builds your first practical German habits, from greetings and introductions to shopping, transport, and simple conversations in Austria.",
price: "EUR 199",
},
{
slug: "intermediate-b1",
level: "B1",
title: "Intermediate",
summary: "Master complex conversations and prepare for work.",
body: "The B1 track strengthens confidence in discussions, emails, workplace communication, and everyday situations that require more independent speaking.",
price: "EUR 249",
},
{
slug: "advanced-c1",
level: "C1",
title: "Advanced",
summary: "Speak fluently in academic and business environments.",
body: "Designed for ambitious learners who need strong precision, persuasive speaking, and advanced comprehension for study, exams, and professional settings.",
price: "EUR 299",
},
],
teachers: [
{ slug: "sarah-mueller", image: images.teachers.sarah, name: "Sarah Mueller", flag: "AT", country: "Austria", title: "Goethe Examiner", description: "Specializes in B2-C1 exam preparation with 10 years of experience." },
{ slug: "lukas-weber", image: images.teachers.lukas, name: "Lukas Weber", flag: "DE", country: "Germany", title: "A1-A2 Specialist", description: "Makes learning grammar fun and engaging for absolute beginners." },
{ slug: "anna-schmidt", image: images.teachers.anna, name: "Anna Schmidt", flag: "AT", country: "Austria", title: "Business German", description: "Helps professionals integrate into the DACH corporate world." },
{ slug: "felix-bauer", image: images.teachers.felix, name: "Felix Bauer", flag: "DE", country: "Germany", title: "Conversation Coach", description: "Focuses on pronunciation and speaking confidence." },
],
testimonials: [
{ text: '"I passed my OeSD B2 exam on the first try. The teachers at Engel are incredibly supportive and the materials are top-notch."', image: images.students.elena, name: "Elena R.", role: "Student Visa Applicant", stars: "full" },
{ text: '"The placement test was very accurate. I started at A2.2 and the pacing of the classes was perfect for someone working full-time."', image: images.students.mark, name: "Mark T.", role: "IT Professional", stars: "full" },
{ text: "\"Love the energy in the online classes. It does not feel like a boring school. It feels like hanging out with friends while learning German.\"", image: images.students.sarah, name: "Sarah J.", role: "University Student", stars: "half" },
],
blogPosts: [
{ slug: "german-articles-hacks", image: images.posts.articles, tagClass: "bg-purple-100 text-brand-purple", tag: "Study Tips", title: "5 Hacks to Remember German Articles", excerpt: "Struggling with noun genders? These simple tricks will save you hours of memorization.", body: "This article explains practical memory systems, pattern recognition, and routine-building strategies to make article memorization less random and more systematic.", date: "Oct 12, 2026" },
{ slug: "meldezettel-vienna", image: images.posts.austria, tagClass: "bg-yellow-100 text-yellow-800", tag: "Life in Austria", title: "How to Register Your Address in Vienna", excerpt: "A step-by-step guide to navigating Austrian bureaucracy as a new student or expat.", body: "We break down the registration process, required documents, timing expectations, and the common mistakes international students make during the first week in Vienna.", date: "Oct 05, 2026" },
{ slug: "osd-vs-goethe", image: images.posts.exams, tagClass: "bg-green-100 text-green-800", tag: "Exams", title: "OeSD vs. Goethe: Which German Exam Should You Take?", excerpt: "Comparing the two most popular German proficiency certificates and helping you choose.", body: "Choosing between OeSD and Goethe depends on your goals, destination, and exam style preference. This guide compares structure, recognition, and preparation strategy.", date: "Sep 28, 2026" },
],
faqItems: [
{ question: "How do I know which level to choose?", answer: "We offer a free 10-minute online placement test. Once completed, our system will recommend the best course for your current proficiency level.", open: false },
{ question: "Are the classes online or in-person?", answer: "We offer both. You can join our campus in Vienna or participate in highly interactive live online classes from anywhere in the world.", open: true },
{ question: "Do you provide certificates after completion?", answer: "Yes. You receive a certificate of attendance, and we also prepare you for official OeSD and Goethe exams.", open: false },
],
cta: {
title: "Ready to speak German fluently?",
description: "Join thousands of successful students. Register today and get access to our exclusive learning portal.",
button: "Create Your Free Account",
},
contact: {
location: "Vienna, Austria",
phone: "+43 1 234 5678",
email: "info@engel-akademie.at",
},
footer: {
description: "Making German learning accessible, energetic, and highly effective in the heart of Austria.",
exploreTitle: "Explore",
contactTitle: "Contact",
links: [
{ label: "All Courses", href: "/{locale}/courses" },
{ label: "Placement Test", href: "/{locale}/placement-test" },
{ label: "Student Portal", href: "/{locale}/contact" },
{ label: "Blog and Tips", href: "/{locale}/blog" },
],
socials: [
{ label: "Instagram", href: "#", icon: "fa-brands fa-instagram" },
{ label: "LinkedIn", href: "#", icon: "fa-brands fa-linkedin-in" },
{ label: "YouTube", href: "#", icon: "fa-brands fa-youtube" },
],
location: "Vienna, Austria",
phone: "+43 1 234 5678",
email: "info@engel-akademie.at",
copyright: "Copyright 2026 DeutschAkademie Engel. All rights reserved.",
privacyLabel: "Privacy Policy",
termsLabel: "Terms of Service",
},
shared: {
coursesTitle: "Choose Your Level",
coursesIntro: "From complete beginners to advanced speakers, we have the right course for you.",
teachersTitle: "Meet Our Native Experts",
teachersIntro: "Learn from passionate, certified instructors from Austria and Germany.",
viewAllTeachers: "View all teachers",
testimonialsTitle: "Student Success Stories",
testimonialsIntro: "Do not just take our word for it. Hear from our community.",
blogTitle: "Latest Tips and News",
blogIntro: "Resources to help you master the German language faster.",
readAllArticles: "Read all articles",
faqTitle: "Frequently Asked Questions",
faqIntro: "Clear answers about levels, formats, and certificates.",
popular: "POPULAR",
perLevel: "level",
selectCourse: "Select Course",
viewDetails: "View Details",
contactTitle: "Contact the Academy",
contactIntro: "Ask about schedules, visas, course formats, or private guidance from the Engel team.",
placementTitle: "Request a Placement Test",
placementIntro: "Tell us about your current German level and goals. We will recommend the right course path.",
nameLabel: "Full name",
emailLabel: "Email address",
phoneLabel: "Phone number",
messageLabel: "Tell us what you need",
sendMessage: "Send Message",
requestPlacement: "Request Placement Test",
},
};
const german = {
...english,
meta: {
title: "DeutschAkademie Engel | Deutsch lernen in Oesterreich",
description: "Lerne Deutsch mit Selbstvertrauen in einer energiegeladenen Sprachakademie in Oesterreich.",
},
navigation: {
homeLabel: "Startseite",
courses: "Kurse",
teachers: "Lehrkraefte",
blog: "Blog",
portal: "Portal",
},
hero: {
badge: "Starte noch heute",
titleStart: "Sprich Deutsch",
titleAccent: "mit Selbstvertrauen.",
description: "Lerne an einer energiegeladenen Sprachakademie in Oesterreich. Beherrsche Deutsch mit muttersprachlichen Expertinnen und Experten, modernen Methoden und einer starken Community.",
primaryCta: "Einstufungstest starten",
secondaryCta: "Kurse ansehen",
},
stats: [
{ value: "50+", label: "Muttersprachliche Lehrkraefte" },
{ value: "A1-C2", label: "Kompletter Lehrplan" },
{ value: "10k+", label: "Zufriedene Lernende" },
{ value: "4.9/5", label: "Durchschnittsbewertung" },
],
courses: [
{ ...english.courses[0], title: "Anfaenger", summary: "Lerne, dich vorzustellen und den Alltag sicher zu meistern.", body: "Dieses Niveau vermittelt dir die ersten praktischen Gewohnheiten auf Deutsch: Begruessungen, Vorstellen, Einkaufen, Verkehr und einfache Gespraeche in Oesterreich." },
{ ...english.courses[1], title: "Mittelstufe", summary: "Fuehre komplexere Gespraeche und bereite dich auf den Beruf vor.", body: "Der B1-Kurs staerkt deine Sicherheit in Gespraechen, E-Mails, Arbeitskontexten und Alltagssituationen, in denen eigenstaendiges Sprechen gefragt ist." },
{ ...english.courses[2], title: "Fortgeschritten", summary: "Sprich fliessend im akademischen und beruflichen Umfeld.", body: "Fuer ambitionierte Lernende, die Praezision, ueberzeugendes Sprechen und fortgeschrittenes Verstaendnis fuer Studium, Pruefungen und Karriere brauchen." },
],
teachers: [
{ ...english.teachers[0], title: "Goethe-Prueferin", country: "Oesterreich", description: "Spezialisiert auf B2-C1 Pruefungsvorbereitung mit 10 Jahren Erfahrung." },
{ ...english.teachers[1], title: "A1-A2 Spezialist", description: "Macht Grammatik fuer absolute Anfaenger verstaendlich und motivierend." },
{ ...english.teachers[2], title: "Wirtschaftsdeutsch", country: "Oesterreich", description: "Unterstuetzt Fachkraefte beim Einstieg in die DACH-Unternehmenswelt." },
{ ...english.teachers[3], title: "Konversationstrainer", description: "Fokussiert auf Aussprache und sicheres freies Sprechen." },
],
testimonials: [
{ ...english.testimonials[0], text: '"Ich habe meine OeSD-B2-Pruefung direkt beim ersten Versuch bestanden. Die Lehrkraefte bei Engel sind unglaublich unterstuetzend und die Materialien sind erstklassig."', role: "Visumsbewerberin" },
{ ...english.testimonials[1], text: '"Der Einstufungstest war sehr praezise. Ich bin auf A2.2 gestartet und das Lerntempo war perfekt fuer jemanden mit Vollzeitjob."', role: "IT-Fachkraft" },
{ ...english.testimonials[2], text: '"Ich liebe die Energie in den Online-Kursen. Es fuehlt sich nicht wie eine langweilige Schule an, sondern wie gemeinsames Lernen mit Freunden."', role: "Studentin" },
],
blogPosts: [
{ ...english.blogPosts[0], tag: "Lerntipps", title: "5 Tricks, um deutsche Artikel besser zu behalten", excerpt: "Du kaempfst mit Nomen und Artikeln? Diese einfachen Methoden sparen dir viel Auswendiglernen.", body: "Dieser Beitrag zeigt praktische Merkhilfen, Mustererkennung und Routinen, mit denen sich Artikel systematischer und nachhaltiger lernen lassen.", date: "12. Okt 2026" },
{ ...english.blogPosts[1], tag: "Leben in Oesterreich", title: "So meldest du deinen Wohnsitz in Wien an", excerpt: "Eine Schritt-fuer-Schritt-Anleitung fuer neue Studierende und Expats in Oesterreich.", body: "Wir erklaeren den Ablauf, die noetigen Unterlagen, typische Fristen und die haeufigsten Fehler bei der ersten Anmeldung in Wien.", date: "05. Okt 2026" },
{ ...english.blogPosts[2], tag: "Pruefungen", title: "OeSD oder Goethe: Welche Deutschpruefung passt zu dir?", excerpt: "Ein Vergleich der beliebtesten Deutschzertifikate und eine Hilfe bei deiner Entscheidung.", body: "Ob OeSD oder Goethe besser passt, haengt von deinem Ziel, deinem Wohnort und deinem bevorzugten Pruefungsstil ab. Dieser Leitfaden vergleicht Anerkennung, Struktur und Vorbereitung.", date: "28. Sep 2026" },
],
faqItems: [
{ question: "Wie finde ich das richtige Niveau?", answer: "Wir bieten einen kostenlosen Online-Einstufungstest in nur 10 Minuten an. Danach empfehlen wir automatisch den passenden Kurs fuer dein aktuelles Sprachniveau.", open: false },
{ question: "Sind die Kurse online oder vor Ort?", answer: "Beides. Du kannst unseren Standort in Wien besuchen oder an interaktiven Live-Online-Kursen von ueberall auf der Welt teilnehmen.", open: true },
{ question: "Bekomme ich nach Abschluss ein Zertifikat?", answer: "Ja. Du erhaeltst ein Teilnahmezertifikat. Ausserdem bereiten wir dich gezielt auf offizielle OeSD- und Goethe-Pruefungen vor.", open: false },
],
cta: {
title: "Bereit, fliessend Deutsch zu sprechen?",
description: "Werde Teil von tausenden erfolgreichen Lernenden. Registriere dich noch heute und erhalte Zugang zu unserem exklusiven Lernportal.",
button: "Kostenloses Konto erstellen",
},
contact: {
location: "Wien, Oesterreich",
phone: "+43 1 234 5678",
email: "info@engel-akademie.at",
},
footer: {
...english.footer,
description: "Wir machen Deutschlernen zugaenglich, energiegeladen und wirkungsvoll im Herzen Oesterreichs.",
exploreTitle: "Entdecken",
contactTitle: "Kontakt",
links: [
{ label: "Alle Kurse", href: "/{locale}/courses" },
{ label: "Einstufungstest", href: "/{locale}/placement-test" },
{ label: "Studentenportal", href: "/{locale}/contact" },
{ label: "Blog und Tipps", href: "/{locale}/blog" },
],
location: "Wien, Oesterreich",
copyright: "Copyright 2026 DeutschAkademie Engel. Alle Rechte vorbehalten.",
privacyLabel: "Datenschutz",
termsLabel: "AGB",
},
shared: {
coursesTitle: "Waehle dein Niveau",
coursesIntro: "Vom absoluten Anfaenger bis zur fortgeschrittenen Sprachkompetenz: Wir haben den passenden Kurs fuer dich.",
teachersTitle: "Unsere muttersprachlichen Expertinnen und Experten",
teachersIntro: "Lerne mit leidenschaftlichen, zertifizierten Lehrkraeften aus Oesterreich und Deutschland.",
viewAllTeachers: "Alle Lehrkraefte ansehen",
testimonialsTitle: "Erfolgsgeschichten unserer Lernenden",
testimonialsIntro: "Nicht nur wir sagen das. Hoere direkt von unserer Community.",
blogTitle: "Neueste Tipps und News",
blogIntro: "Ressourcen, mit denen du Deutsch schneller und gezielter lernst.",
readAllArticles: "Alle Artikel lesen",
faqTitle: "Haeufig gestellte Fragen",
faqIntro: "Klare Antworten zu Niveaus, Formaten und Zertifikaten.",
popular: "BELIEBT",
perLevel: "Niveau",
selectCourse: "Kurs waehlen",
viewDetails: "Details ansehen",
contactTitle: "Kontaktiere die Akademie",
contactIntro: "Frage nach Terminen, Visa, Kursformaten oder individueller Beratung durch das Engel-Team.",
placementTitle: "Einstufungstest anfragen",
placementIntro: "Erzaehl uns mehr ueber dein aktuelles Deutschniveau und deine Ziele. Wir empfehlen dir den passenden Kursweg.",
nameLabel: "Vollstaendiger Name",
emailLabel: "E-Mail-Adresse",
phoneLabel: "Telefonnummer",
messageLabel: "Wobei brauchst du Hilfe?",
sendMessage: "Nachricht senden",
requestPlacement: "Einstufung anfragen",
},
};
const baseData = { en: english, de: german };
export function isSupportedLocale(locale) {
return locales.includes(locale);
}
export function getLandingContent(locale) {
return baseData[locale] ?? baseData.en;
}
export function getCourseBySlug(locale, slug) {
return getLandingContent(locale).courses.find((course) => course.slug === slug);
}
export function getBlogPostBySlug(locale, slug) {
return getLandingContent(locale).blogPosts.find((post) => post.slug === slug);
}

9
next.config.mjs Normal file
View File

@@ -0,0 +1,9 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
formats: ["image/avif", "image/webp"],
},
};
export default nextConfig;

1597
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

27
package.json Normal file
View File

@@ -0,0 +1,27 @@
{
"name": "academy-engel-next",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@fontsource/poppins": "^5.2.7",
"@prisma/client": "^6.7.0",
"@tailwindcss/postcss": "^4.2.2",
"aos": "^2.3.4",
"bcryptjs": "^3.0.2",
"next": "^15.3.0",
"next-auth": "^5.0.0-beta.25",
"postcss": "^8.5.9",
"prisma": "^6.7.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-icons": "^5.6.0",
"tailwindcss": "^4.2.2",
"zod": "^3.24.4"
}
}

5
postcss.config.mjs Normal file
View File

@@ -0,0 +1,5 @@
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};

133
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,133 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
enum UserRole {
ADMIN
EDITOR
}
enum ContentStatus {
DRAFT
PUBLISHED
}
enum LeadType {
CONTACT
PLACEMENT
}
enum LeadStatus {
NEW
REVIEWED
CLOSED
}
model User {
id String @id @default(cuid())
name String
email String @unique
passwordHash String
role UserRole @default(EDITOR)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Course {
id String @id @default(cuid())
slug String @unique
level String
price String
imageUrl String?
status ContentStatus @default(DRAFT)
sortOrder Int @default(0)
titleEn String
titleDe String
summaryEn String
summaryDe String
bodyEn String
bodyDe String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Teacher {
id String @id @default(cuid())
slug String @unique
imageUrl String?
flagCode String?
nationalityEn String?
nationalityDe String?
status ContentStatus @default(DRAFT)
sortOrder Int @default(0)
nameEn String
nameDe String
titleEn String
titleDe String
bioEn String
bioDe String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Post {
id String @id @default(cuid())
slug String @unique
coverImageUrl String?
categoryEn String
categoryDe String
status ContentStatus @default(DRAFT)
publishedAt DateTime?
titleEn String
titleDe String
excerptEn String
excerptDe String
bodyEn String
bodyDe String
seoTitleEn String?
seoTitleDe String?
seoDescriptionEn String?
seoDescriptionDe String?
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model FaqItem {
id String @id @default(cuid())
key String @unique
sortOrder Int @default(0)
questionEn String
questionDe String
answerEn String
answerDe String
active Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model Lead {
id String @id @default(cuid())
type LeadType
name String
email String
phone String?
message String?
preferredLevel String?
locale String
status LeadStatus @default(NEW)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
model SiteSetting {
id String @id @default(cuid())
key String @unique
value Json
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}

1
prisma/seed.mjs Normal file
View File

@@ -0,0 +1 @@
console.log("Seed placeholder: install Prisma dependencies and wire database access before running seeds.");

Binary file not shown.

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 269 KiB