Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- import { createFileRoute, notFound, Link } from "@tanstack/react-router";
- // 1. PERBAIKAN: Import dari movie-config, dan ambil MovieBundle langsung dari sana
- import { fetchMovieBundle, MOVIE_CONFIG, type MovieBundle } from "@/lib/movie-config";
- // 2. SEMENTARA KITA MATIKAN DULU IKLANNYA agar proses Build sukses.
- // Nanti kita perbaiki komponen iklan setelah website berhasil online.
- // import { AtOptionsAd, NativeBannerAd } from "@/components/AdBanner";
- export const Route = createFileRoute("/movie/$slug")({
- loader: async ({ params }): Promise<MovieBundle> => {
- const match = params.slug.match(/^(\d+)(?:-(.*))?$/);
- if (!match) throw notFound();
- const id = match[1]; // Biarkan sebagai string (teks)
- // 3. PERBAIKAN: Gunakan movieId, bukan id
- if (id !== MOVIE_CONFIG.movieId) throw notFound();
- // 4. PERBAIKAN: Kosongkan tanda kurung pada fetchMovieBundle()
- return await fetchMovieBundle();
- },
- head: ({ loaderData, params }) => {
- if (!loaderData) {
- return { meta: [{ title: "Film nicht gefunden" }, { name: "robots", content: "noindex" }] };
- }
- const m = loaderData.details;
- const url = `/movie/${params.movieSlug}`;
- const year = m.release_date?.slice(0, 4) ?? "";
- const primaryGenre = m.genres[0]?.name ?? "Marvel";
- const title = `${m.title} (${year}) Ganzer Film Deutsch Stream Online anschauen`;
- const desc = `${m.title} (${year}) Ganzer Film auf Deutsch online schauen. Erleben Sie den neuesten ${primaryGenre}-Blockbuster im HD-Stream ohne Anmeldung. Jetzt streamen!`;
- const image = m.backdrop_path ? `${IMG_BASE}/w1280${m.backdrop_path}` : undefined;
- const meta: Array<Record<string, string>> = [
- { title },
- { name: "description", content: desc },
- { name: "robots", content: "index, follow, max-image-preview:large, max-snippet:-1" },
- { property: "og:title", content: title },
- { property: "og:description", content: desc },
- { property: "og:type", content: "video.movie" },
- { property: "og:url", content: url },
- { property: "og:locale", content: "de_DE" },
- { name: "twitter:card", content: "summary_large_image" },
- { name: "twitter:title", content: title },
- { name: "twitter:description", content: desc },
- ];
- if (image) {
- meta.push({ property: "og:image", content: image });
- meta.push({ name: "twitter:image", content: image });
- }
- return {
- meta,
- links: [{ rel: "canonical", href: url }],
- scripts: [
- {
- type: "application/ld+json",
- children: JSON.stringify({
- "@context": "https://schema.org",
- "@type": "Movie",
- name: m.title,
- alternateName: m.original_title,
- description: m.overview,
- image: image,
- datePublished: m.release_date,
- genre: m.genres.map((g) => g.name),
- aggregateRating: m.vote_count
- ? {
- "@type": "AggregateRating",
- ratingValue: m.vote_average,
- ratingCount: m.vote_count,
- bestRating: 10,
- }
- : undefined,
- director: loaderData.credits.crew
- .filter((c) => c.job === "Director")
- .map((c) => ({ "@type": "Person", name: c.name })),
- actor: loaderData.credits.cast.slice(0, 8).map((c) => ({ "@type": "Person", name: c.name })),
- }),
- },
- {
- type: "application/ld+json",
- children: JSON.stringify({
- "@context": "https://schema.org",
- "@type": "BreadcrumbList",
- itemListElement: [
- { "@type": "ListItem", position: 1, name: "Home", item: "/" },
- { "@type": "ListItem", position: 2, name: m.title, item: url },
- ],
- }),
- },
- ],
- };
- },
- errorComponent: ({ error }) => (
- <div className="flex min-h-screen items-center justify-center p-6 text-center">
- <div>
- <h1 className="text-2xl font-bold">Fehler beim Laden</h1>
- <p className="mt-2 text-muted-foreground">{error.message}</p>
- <Link to="/" className="mt-4 inline-block text-primary underline">Zur Startseite</Link>
- </div>
- </div>
- ),
- notFoundComponent: () => (
- <div className="flex min-h-screen items-center justify-center p-6 text-center">
- <div>
- <h1 className="text-2xl font-bold">Film nicht gefunden</h1>
- <Link to="/" className="mt-4 inline-block text-primary underline">Zur Startseite</Link>
- </div>
- </div>
- ),
- component: MoviePage,
- });
- function formatCurrency(n: number) {
- if (!n) return "—";
- return new Intl.NumberFormat("de-DE", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(n);
- }
- function formatDate(d: string) {
- if (!d) return "—";
- return new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(d));
- }
- function formatRuntime(min: number | null) {
- if (!min) return "—";
- const h = Math.floor(min / 60);
- const m = min % 60;
- return `${h} Std. ${m} Min.`;
- }
- const STATUS_DE: Record<string, string> = {
- Released: "Veröffentlicht",
- "Post Production": "Post-Produktion",
- "In Production": "In Produktion",
- Planned: "Geplant",
- Rumored: "Gerücht",
- Canceled: "Abgesagt",
- };
- function MoviePage() {
- const { details: m, credits, videos, recommendations } = Route.useLoaderData() as MovieBundle;
- const backdrop = m.backdrop_path ? `${IMG_BASE}/original${m.backdrop_path}` : null;
- const poster = m.poster_path ? `${IMG_BASE}/w500${m.poster_path}` : null;
- const trailer = videos.results.find(
- (v) => v.site === "YouTube" && v.type === "Trailer" && v.official
- ) || videos.results.find((v) => v.site === "YouTube" && v.type === "Trailer")
- || videos.results.find((v) => v.site === "YouTube");
- const directors = credits.crew.filter((c) => c.job === "Director");
- const writers = credits.crew.filter((c) =>
- ["Screenplay", "Writer", "Story", "Author"].includes(c.job)
- );
- const cast = credits.cast.slice(0, 10);
- const recs = recommendations.results.slice(0, 6);
- return (
- <div className="min-h-screen bg-background pb-20">
- {/* Hero */}
- <section className="relative isolate overflow-hidden">
- {backdrop && (
- <img
- src={backdrop}
- alt=""
- loading="eager"
- className="absolute inset-0 h-full w-full object-cover opacity-30"
- />
- )}
- <div className="absolute inset-0 bg-gradient-to-b from-background/60 via-background/80 to-background" />
- <div className="relative mx-auto grid max-w-6xl gap-8 px-4 py-16 md:grid-cols-[280px_1fr] md:py-24">
- {poster && (
- <img
- src={poster}
- alt={`Poster von ${m.title}`}
- className="mx-auto w-56 rounded-2xl shadow-2xl md:w-full"
- loading="eager"
- />
- )}
- <div>
- <nav aria-label="Breadcrumb" className="mb-3 text-xs text-muted-foreground">
- <Link to="/" className="hover:underline">Startseite</Link>
- <span className="mx-2">/</span>
- <span>{m.title}</span>
- </nav>
- <h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl md:text-5xl">
- {m.title}
- </h1>
- {m.original_title !== m.title && (
- <p className="mt-1 text-sm text-muted-foreground">
- Originaltitel: {m.original_title}
- </p>
- )}
- {m.tagline && <p className="mt-3 italic text-muted-foreground">"{m.tagline}"</p>}
- <div className="mt-5 flex flex-wrap gap-2">
- {m.genres.map((g) => (
- <span
- key={g.id}
- className="rounded-full bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground"
- >
- {g.name}
- </span>
- ))}
- </div>
- <div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
- <span>⭐ {m.vote_average.toFixed(1)} / 10 ({m.vote_count.toLocaleString("de-DE")} Stimmen)</span>
- <span>{formatRuntime(m.runtime)}</span>
- <span>{formatDate(m.release_date)}</span>
- </div>
- <a
- href={MOVIE_CONFIG.watchUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="mt-8 inline-flex items-center justify-center rounded-full bg-primary px-10 py-4 text-lg font-semibold text-primary-foreground shadow-xl transition-transform hover:scale-105"
- >
- ▶ {MOVIE_CONFIG.watchText}
- </a>
- </div>
- </div>
- </section>
- <div className="mx-auto max-w-6xl space-y-8 px-4">
- {/* Ad: leaderboard (728x90 desktop, 320x50 mobile) */}
- <div className="hidden justify-center md:flex">
- <AtOptionsAd adKey="add579d28cd593077da0cb857a4cab32" width={728} height={90} />
- </div>
- <div className="flex justify-center md:hidden">
- <AtOptionsAd adKey="b6529f582f191a94d4ed6d9700cbbfba" width={320} height={50} />
- </div>
- {/* Synopsis */}
- <Card title="Handlung">
- <p className="leading-relaxed text-foreground/90">{m.overview || "Keine Beschreibung verfügbar."}</p>
- </Card>
- {/* Trailer */}
- {trailer && (
- <Card title="Offizieller Trailer">
- <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-black">
- <iframe
- src={`https://www.youtube-nocookie.com/embed/${trailer.key}`}
- title={`Trailer: ${m.title}`}
- loading="lazy"
- allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
- allowFullScreen
- className="absolute inset-0 h-full w-full"
- />
- </div>
- </Card>
- )}
- {/* Ad: native banner + 468x60 */}
- <NativeBannerAd />
- <div className="hidden justify-center sm:flex">
- <AtOptionsAd adKey="5da568282f66fb9805ae7e33c2d98d6f" width={468} height={60} />
- </div>
- {/* Cast */}
- {cast.length > 0 && (
- <Card title="Besetzung">
- <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5">
- {cast.map((c) => (
- <div key={c.id} className="text-center">
- <div className="mx-auto aspect-[2/3] w-full overflow-hidden rounded-lg bg-muted">
- {c.profile_path ? (
- <img
- src={`${IMG_BASE}/w185${c.profile_path}`}
- alt={c.name}
- loading="lazy"
- className="h-full w-full object-cover"
- />
- ) : (
- <div className="flex h-full items-center justify-center text-xs text-muted-foreground">
- Kein Bild
- </div>
- )}
- </div>
- <p className="mt-2 text-sm font-medium text-foreground">{c.name}</p>
- <p className="text-xs text-muted-foreground">{c.character}</p>
- </div>
- ))}
- </div>
- </Card>
- )}
- {/* Directors / Writers */}
- <div className="grid gap-8 md:grid-cols-2">
- <Card title="Regie">
- {directors.length ? (
- <ul className="space-y-1">
- {directors.map((d) => (
- <li key={d.id} className="text-foreground">{d.name}</li>
- ))}
- </ul>
- ) : (
- <p className="text-muted-foreground">—</p>
- )}
- </Card>
- <Card title="Drehbuch">
- {writers.length ? (
- <ul className="space-y-1">
- {writers.map((w) => (
- <li key={`${w.id}-${w.job}`} className="text-foreground">
- {w.name} <span className="text-xs text-muted-foreground">({w.job})</span>
- </li>
- ))}
- </ul>
- ) : (
- <p className="text-muted-foreground">—</p>
- )}
- </Card>
- </div>
- {/* Facts */}
- <Card title="Film-Fakten">
- <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
- <Fact label="Status" value={STATUS_DE[m.status] ?? m.status} />
- <Fact label="Originalsprache" value={m.original_language?.toUpperCase()} />
- <Fact label="Veröffentlichung" value={formatDate(m.release_date)} />
- <Fact label="Laufzeit" value={formatRuntime(m.runtime)} />
- <Fact label="Budget" value={formatCurrency(m.budget)} />
- <Fact label="Einspielergebnis" value={formatCurrency(m.revenue)} />
- <Fact label="TMDb-Bewertung" value={`${m.vote_average.toFixed(1)} / 10`} />
- <Fact label="Anzahl Stimmen" value={m.vote_count.toLocaleString("de-DE")} />
- <Fact
- label="Gesprochene Sprachen"
- value={m.spoken_languages.map((s) => s.name).join(", ") || "—"}
- />
- </dl>
- </Card>
- {/* Production */}
- <Card title="Produktion">
- <div className="space-y-4">
- <div>
- <h3 className="text-sm font-medium text-muted-foreground">Produktionsfirmen</h3>
- <p className="mt-1 text-foreground">
- {m.production_companies.map((c) => c.name).join(", ") || "—"}
- </p>
- </div>
- <div>
- <h3 className="text-sm font-medium text-muted-foreground">Produktionsländer</h3>
- <p className="mt-1 text-foreground">
- {m.production_countries.map((c) => c.name).join(", ") || "—"}
- </p>
- </div>
- </div>
- </Card>
- {/* Recommendations */}
- {recs.length > 0 && (
- <Card title="Ähnliche Filme">
- <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-6">
- {recs.map((r) => (
- <Link
- key={r.id}
- to="/movie/$movieSlug"
- params={{ movieSlug: `${r.id}-${MOVIE_CONFIG.slug}` }}
- className="group"
- >
- <div className="aspect-[2/3] overflow-hidden rounded-lg bg-muted">
- {r.poster_path ? (
- <img
- src={`${IMG_BASE}/w342${r.poster_path}`}
- alt={r.title}
- loading="lazy"
- className="h-full w-full object-cover transition-transform group-hover:scale-105"
- />
- ) : null}
- </div>
- <p className="mt-2 line-clamp-2 text-sm font-medium text-foreground">{r.title}</p>
- <p className="text-xs text-muted-foreground">
- {r.release_date?.slice(0, 4) || ""}
- </p>
- </Link>
- ))}
- </div>
- </Card>
- )}
- {/* Ad: 160x600 skyscraper (centered on desktop) */}
- <div className="hidden justify-center lg:flex">
- <AtOptionsAd adKey="00de6b5d88ffd4850a38f045cca6bb12" width={160} height={600} />
- </div>
- {/* Big watch button */}
- <div className="pt-6 text-center">
- <a
- href={MOVIE_CONFIG.watchUrl}
- target="_blank"
- rel="noopener noreferrer"
- className="inline-flex items-center justify-center rounded-full bg-primary px-12 py-5 text-xl font-bold text-primary-foreground shadow-2xl transition-transform hover:scale-105"
- >
- ▶ {MOVIE_CONFIG.watchText}
- </a>
- </div>
- </div>
- </div>
- );
- }
- function Card({ title, children }: { title: string; children: React.ReactNode }) {
- return (
- <section className="rounded-2xl border border-border bg-card p-6 shadow-sm md:p-8">
- <h2 className="mb-4 text-xl font-semibold text-card-foreground md:text-2xl">{title}</h2>
- {children}
- </section>
- );
- }
- function Fact({ label, value }: { label: string; value: string }) {
- return (
- <div>
- <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{label}</dt>
- <dd className="mt-1 text-foreground">{value}</dd>
- </div>
- );
- }
Advertisement
Add Comment
Please, Sign In to add comment