diff --git a/src/app/(protected)/admin/listings/[slug]/page.tsx b/src/app/(protected)/admin/listings/[slug]/page.tsx
new file mode 100644
index 0000000..c1a1ebf
--- /dev/null
+++ b/src/app/(protected)/admin/listings/[slug]/page.tsx
@@ -0,0 +1,349 @@
+import { prisma } from "@/lib/prisma";
+import { redirect } from "next/navigation";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { updateListing, deleteListing, addNote, addTagToListing, removeTagFromListing } from "../actions";
+
+export default async function EditListingPage({
+ params,
+}: {
+ params: { slug: string };
+}) {
+ const listing = await prisma.listing.findUnique({
+ where: { slug: params.slug },
+ include: {
+ tags: {
+ include: { tag: true },
+ },
+ notes: {
+ orderBy: { createdAt: "desc" },
+ },
+ },
+ });
+
+ if (!listing) {
+ redirect("/listings");
+ }
+
+ const allTags = await prisma.tag.findMany({
+ orderBy: { name: "asc" },
+ });
+
+ const listingTagIds = listing.tags.map((t) => t.tagId);
+ const availableTags = allTags.filter((t) => !listingTagIds.includes(t.id));
+
+ return (
+
+
+
+
+
+ {/* Main Form */}
+
+
+
+ Grunddaten
+
+
+
+
+
+
+
+
+
+ {/* Sidebar */}
+
+ {/* Tags */}
+
+
+ 🏷️ Tags
+
+
+
+ {listing.tags.map((lt) => (
+
+ {lt.tag.name}
+
+
+ ))}
+ {listing.tags.length === 0 && (
+
Keine Tags
+ )}
+
+
+ {availableTags.length > 0 && (
+
+ )}
+
+
+
+ {/* Notes */}
+
+
+ 📝 Notizen
+
+
+
+ {listing.notes.map((note) => (
+
+ {note.body}
+
+ {new Date(note.createdAt).toLocaleDateString("de-DE")}
+
+
+ ))}
+ {listing.notes.length === 0 && (
+
Keine Notizen
+ )}
+
+
+
+
+
+
+ {/* Info */}
+
+
+ ℹ️ Info
+
+
+
+ Importiert:{" "}
+ {new Date(listing.importedAt).toLocaleDateString("de-DE")}
+
+
+ Airbnb URL:
+
+
+ {listing.airbnbUrl}
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/admin/listings/actions.ts b/src/app/(protected)/admin/listings/actions.ts
new file mode 100644
index 0000000..a530ef1
--- /dev/null
+++ b/src/app/(protected)/admin/listings/actions.ts
@@ -0,0 +1,117 @@
+"use server";
+
+import { prisma } from "@/lib/prisma";
+import { revalidatePath } from "next/cache";
+import { redirect } from "next/navigation";
+
+export async function updateListing(formData: FormData) {
+ const id = formData.get("id") as string;
+ const title = formData.get("title") as string;
+ const locationText = formData.get("locationText") as string;
+ const nightlyPrice = parseFloat(formData.get("nightlyPrice") as string) || null;
+ const rating = parseFloat(formData.get("rating") as string) || null;
+ const reviewCount = parseInt(formData.get("reviewCount") as string) || null;
+ const bedrooms = parseInt(formData.get("bedrooms") as string) || null;
+ const beds = parseInt(formData.get("beds") as string) || null;
+ const bathrooms = parseFloat(formData.get("bathrooms") as string) || null;
+ const guestCount = parseInt(formData.get("guestCount") as string) || null;
+ const maxSleepingPlaces = parseInt(formData.get("maxSleepingPlaces") as string) || null;
+ const hostName = formData.get("hostName") as string;
+ const description = formData.get("description") as string;
+ const status = formData.get("status") as string;
+ const isFavorite = formData.get("isFavorite") === "true";
+
+ await prisma.listing.update({
+ where: { id },
+ data: {
+ title,
+ locationText,
+ nightlyPrice,
+ rating,
+ reviewCount,
+ bedrooms,
+ beds,
+ bathrooms,
+ guestCount,
+ maxSleepingPlaces,
+ hostName,
+ description,
+ status,
+ isFavorite,
+ },
+ });
+
+ revalidatePath("/listings");
+ revalidatePath(`/listings/${id}`);
+ redirect(`/listings`);
+}
+
+export async function deleteListing(formData: FormData) {
+ const id = formData.get("id") as string;
+
+ await prisma.listing.delete({
+ where: { id },
+ });
+
+ revalidatePath("/listings");
+ redirect("/listings");
+}
+
+export async function toggleFavorite(formData: FormData) {
+ const id = formData.get("id") as string;
+ const isFavorite = formData.get("isFavorite") === "true";
+
+ await prisma.listing.update({
+ where: { id },
+ data: { isFavorite: !isFavorite },
+ });
+
+ revalidatePath("/listings");
+ revalidatePath(`/listings/${id}`);
+}
+
+export async function addNote(formData: FormData) {
+ const listingId = formData.get("listingId") as string;
+ const body = formData.get("body") as string;
+
+ if (!body?.trim()) return;
+
+ await prisma.adminNote.create({
+ data: {
+ listingId,
+ body: body.trim(),
+ },
+ });
+
+ revalidatePath(`/listings/${listingId}`);
+}
+
+export async function addTagToListing(formData: FormData) {
+ const listingId = formData.get("listingId") as string;
+ const tagId = formData.get("tagId") as string;
+
+ await prisma.listingTag.create({
+ data: {
+ listingId,
+ tagId,
+ },
+ });
+
+ revalidatePath(`/listings/${listingId}`);
+}
+
+export async function removeTagFromListing(formData: FormData) {
+ const listingId = formData.get("listingId") as string;
+ const tagId = formData.get("tagId") as string;
+
+ await prisma.listingTag.delete({
+ where: {
+ listingId_tagId: {
+ listingId,
+ tagId,
+ },
+ },
+ });
+
+ revalidatePath(`/listings/${listingId}`);
+}
diff --git a/src/app/(protected)/admin/tags/actions.ts b/src/app/(protected)/admin/tags/actions.ts
new file mode 100644
index 0000000..b9b7da6
--- /dev/null
+++ b/src/app/(protected)/admin/tags/actions.ts
@@ -0,0 +1,33 @@
+"use server";
+
+import { prisma } from "@/lib/prisma";
+import { revalidatePath } from "next/cache";
+
+export async function createTag(formData: FormData) {
+ const name = formData.get("name") as string;
+ const color = formData.get("color") as string;
+
+ if (!name) return;
+
+ const slug = name.toLowerCase().replace(/\s+/g, "-");
+
+ await prisma.tag.upsert({
+ where: { slug },
+ update: { color },
+ create: { name, slug, color },
+ });
+
+ revalidatePath("/admin/tags");
+}
+
+export async function deleteTag(formData: FormData) {
+ const tagId = formData.get("tagId") as string;
+
+ if (!tagId) return;
+
+ await prisma.tag.delete({
+ where: { id: tagId },
+ });
+
+ revalidatePath("/admin/tags");
+}
diff --git a/src/app/(protected)/admin/tags/page.tsx b/src/app/(protected)/admin/tags/page.tsx
new file mode 100644
index 0000000..63494f3
--- /dev/null
+++ b/src/app/(protected)/admin/tags/page.tsx
@@ -0,0 +1,119 @@
+import { prisma } from "@/lib/prisma";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+import { createTag, deleteTag } from "./actions";
+
+export default async function TagsPage() {
+ const tags = await prisma.tag.findMany({
+ include: {
+ _count: {
+ select: { listings: true },
+ },
+ },
+ orderBy: { name: "asc" },
+ });
+
+ const defaultColors = [
+ "#6366f1", // Indigo
+ "#10b981", // Emerald
+ "#f59e0b", // Amber
+ "#ef4444", // Red
+ "#8b5cf6", // Violet
+ "#ec4899", // Pink
+ "#14b8a6", // Teal
+ "#f97316", // Orange
+ ];
+
+ return (
+
+
+
🏷️ Tags verwalten
+
+
+ {/* Create Tag Form */}
+
+
+ Neuen Tag erstellen
+
+
+
+
+
+
+ {/* Existing Tags */}
+
+
+ Alle Tags ({tags.length})
+
+
+
+ {tags.map((tag) => (
+
+
+
+
{tag.name}
+
+ ({tag._count.listings} Listings)
+
+
+
+
+ ))}
+ {tags.length === 0 && (
+
+ Noch keine Tags vorhanden
+
+ )}
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/(protected)/layout.tsx b/src/app/(protected)/layout.tsx
new file mode 100644
index 0000000..8b96615
--- /dev/null
+++ b/src/app/(protected)/layout.tsx
@@ -0,0 +1,89 @@
+import { auth, signOut } from "@/lib/auth";
+import Link from "next/link";
+import { redirect } from "next/navigation";
+import { Button } from "@/components/ui/button";
+
+export default async function ProtectedLayout({
+ children,
+}: {
+ children: React.ReactNode;
+}) {
+ const session = await auth();
+
+ if (!session) {
+ redirect("/login");
+ }
+
+ return (
+
+ {/* Navigation */}
+
+
+ {/* Main Content */}
+
{children}
+
+ );
+}
diff --git a/src/app/api/auth/[...nextauth]/route.ts b/src/app/api/auth/[...nextauth]/route.ts
new file mode 100644
index 0000000..c55a45e
--- /dev/null
+++ b/src/app/api/auth/[...nextauth]/route.ts
@@ -0,0 +1,3 @@
+import { handlers } from "@/lib/auth";
+
+export const { GET, POST } = handlers;
diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx
new file mode 100644
index 0000000..47bb6cb
--- /dev/null
+++ b/src/app/login/page.tsx
@@ -0,0 +1,53 @@
+import { signIn } from "@/lib/auth";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export default function LoginPage() {
+ return (
+
+ );
+}
diff --git a/src/app/page.tsx b/src/app/page.tsx
index 295f8fd..a74cb27 100644
--- a/src/app/page.tsx
+++ b/src/app/page.tsx
@@ -1,65 +1,5 @@
-import Image from "next/image";
+import { redirect } from "next/navigation";
export default function Home() {
- return (
-
-
-
-
-
- To get started, edit the page.tsx file.
-
-
- Looking for a starting point or more instructions? Head over to{" "}
-
- Templates
- {" "}
- or the{" "}
-
- Learning
- {" "}
- center.
-
-
-
-
-
- );
+ redirect("/dashboard");
}
diff --git a/src/middleware.ts b/src/middleware.ts
new file mode 100644
index 0000000..ac8c48b
--- /dev/null
+++ b/src/middleware.ts
@@ -0,0 +1,29 @@
+import { auth } from "@/lib/auth";
+import { NextResponse } from "next/server";
+
+export default auth((req) => {
+ const isLoggedIn = !!req.auth;
+ const isOnLogin = req.nextUrl.pathname.startsWith("/login");
+ const isApiRoute = req.nextUrl.pathname.startsWith("/api");
+
+ // Allow API routes (they handle their own auth)
+ if (isApiRoute) {
+ return NextResponse.next();
+ }
+
+ // Redirect to login if not authenticated
+ if (!isLoggedIn && !isOnLogin) {
+ return NextResponse.redirect(new URL("/login", req.nextUrl));
+ }
+
+ // Redirect to dashboard if already logged in and on login page
+ if (isLoggedIn && isOnLogin) {
+ return NextResponse.redirect(new URL("/dashboard", req.nextUrl));
+ }
+
+ return NextResponse.next();
+});
+
+export const config = {
+ matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
+};