1st
3
.env.example
Normal 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
@@ -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
@@ -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.
|
||||||
41
app/[locale]/blog/[slug]/page.js
Normal 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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
49
app/[locale]/contact/page.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
app/[locale]/courses/[slug]/page.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
48
app/[locale]/courses/page.js
Normal 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
@@ -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
@@ -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)} />;
|
||||||
|
}
|
||||||
48
app/[locale]/placement-test/page.js
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
45
app/[locale]/teachers/page.js
Normal 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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default function RootPage() {
|
||||||
|
redirect("/en");
|
||||||
|
}
|
||||||
34
components/site/faq-accordion.js
Normal 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
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
230
components/site/landing-page.js
Normal 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
@@ -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 →</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 →</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>© 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
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
284
lib/site-data.js
Normal 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
@@ -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
27
package.json
Normal 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
@@ -0,0 +1,5 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
133
prisma/schema.prisma
Normal 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
@@ -0,0 +1 @@
|
|||||||
|
console.log("Seed placeholder: install Prisma dependencies and wire database access before running seeds.");
|
||||||
BIN
public/images/blog-articles.jpg
Normal file
|
After Width: | Height: | Size: 103 KiB |
BIN
public/images/blog-austria.jpg
Normal file
|
After Width: | Height: | Size: 164 KiB |
BIN
public/images/blog-exams.jpg
Normal file
|
After Width: | Height: | Size: 95 KiB |
BIN
public/images/hero-students.jpg
Normal file
|
After Width: | Height: | Size: 249 KiB |
BIN
public/images/student-elena.jpg
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
public/images/student-mark.jpg
Normal file
|
After Width: | Height: | Size: 6.6 KiB |
BIN
public/images/student-sarah.jpg
Normal file
|
After Width: | Height: | Size: 5.3 KiB |
BIN
public/images/teacher-anna.jpg
Normal file
|
After Width: | Height: | Size: 155 KiB |
BIN
public/images/teacher-felix.jpg
Normal file
|
After Width: | Height: | Size: 102 KiB |
BIN
public/images/teacher-lukas.jpg
Normal file
|
After Width: | Height: | Size: 185 KiB |
BIN
public/images/teacher-sarah.jpg
Normal file
|
After Width: | Height: | Size: 269 KiB |