E3R

sda

E3R
Jul 20th, 2026
10
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.75 KB | None | 0 0
  1. import { createFileRoute, notFound, Link } from "@tanstack/react-router";
  2.  
  3. // 1. PERBAIKAN: Import dari movie-config, dan ambil MovieBundle langsung dari sana
  4. import { fetchMovieBundle, MOVIE_CONFIG, type MovieBundle } from "@/lib/movie-config";
  5.  
  6. // 2. SEMENTARA KITA MATIKAN DULU IKLANNYA agar proses Build sukses.
  7. // Nanti kita perbaiki komponen iklan setelah website berhasil online.
  8. // import { AtOptionsAd, NativeBannerAd } from "@/components/AdBanner";
  9.  
  10. export const Route = createFileRoute("/movie/$slug")({
  11. loader: async ({ params }): Promise<MovieBundle> => {
  12. const match = params.slug.match(/^(\d+)(?:-(.*))?$/);
  13. if (!match) throw notFound();
  14.  
  15. const id = match[1]; // Biarkan sebagai string (teks)
  16.  
  17. // 3. PERBAIKAN: Gunakan movieId, bukan id
  18. if (id !== MOVIE_CONFIG.movieId) throw notFound();
  19.  
  20. // 4. PERBAIKAN: Kosongkan tanda kurung pada fetchMovieBundle()
  21. return await fetchMovieBundle();
  22. },
  23. head: ({ loaderData, params }) => {
  24. if (!loaderData) {
  25. return { meta: [{ title: "Film nicht gefunden" }, { name: "robots", content: "noindex" }] };
  26. }
  27. const m = loaderData.details;
  28. const url = `/movie/${params.movieSlug}`;
  29. const year = m.release_date?.slice(0, 4) ?? "";
  30. const primaryGenre = m.genres[0]?.name ?? "Marvel";
  31. const title = `${m.title} (${year}) Ganzer Film Deutsch Stream Online anschauen`;
  32. const desc = `${m.title} (${year}) Ganzer Film auf Deutsch online schauen. Erleben Sie den neuesten ${primaryGenre}-Blockbuster im HD-Stream ohne Anmeldung. Jetzt streamen!`;
  33. const image = m.backdrop_path ? `${IMG_BASE}/w1280${m.backdrop_path}` : undefined;
  34. const meta: Array<Record<string, string>> = [
  35. { title },
  36. { name: "description", content: desc },
  37. { name: "robots", content: "index, follow, max-image-preview:large, max-snippet:-1" },
  38. { property: "og:title", content: title },
  39. { property: "og:description", content: desc },
  40. { property: "og:type", content: "video.movie" },
  41. { property: "og:url", content: url },
  42. { property: "og:locale", content: "de_DE" },
  43. { name: "twitter:card", content: "summary_large_image" },
  44. { name: "twitter:title", content: title },
  45. { name: "twitter:description", content: desc },
  46. ];
  47. if (image) {
  48. meta.push({ property: "og:image", content: image });
  49. meta.push({ name: "twitter:image", content: image });
  50. }
  51. return {
  52. meta,
  53. links: [{ rel: "canonical", href: url }],
  54. scripts: [
  55. {
  56. type: "application/ld+json",
  57. children: JSON.stringify({
  58. "@context": "https://schema.org",
  59. "@type": "Movie",
  60. name: m.title,
  61. alternateName: m.original_title,
  62. description: m.overview,
  63. image: image,
  64. datePublished: m.release_date,
  65. genre: m.genres.map((g) => g.name),
  66. aggregateRating: m.vote_count
  67. ? {
  68. "@type": "AggregateRating",
  69. ratingValue: m.vote_average,
  70. ratingCount: m.vote_count,
  71. bestRating: 10,
  72. }
  73. : undefined,
  74. director: loaderData.credits.crew
  75. .filter((c) => c.job === "Director")
  76. .map((c) => ({ "@type": "Person", name: c.name })),
  77. actor: loaderData.credits.cast.slice(0, 8).map((c) => ({ "@type": "Person", name: c.name })),
  78. }),
  79. },
  80. {
  81. type: "application/ld+json",
  82. children: JSON.stringify({
  83. "@context": "https://schema.org",
  84. "@type": "BreadcrumbList",
  85. itemListElement: [
  86. { "@type": "ListItem", position: 1, name: "Home", item: "/" },
  87. { "@type": "ListItem", position: 2, name: m.title, item: url },
  88. ],
  89. }),
  90. },
  91. ],
  92. };
  93. },
  94. errorComponent: ({ error }) => (
  95. <div className="flex min-h-screen items-center justify-center p-6 text-center">
  96. <div>
  97. <h1 className="text-2xl font-bold">Fehler beim Laden</h1>
  98. <p className="mt-2 text-muted-foreground">{error.message}</p>
  99. <Link to="/" className="mt-4 inline-block text-primary underline">Zur Startseite</Link>
  100. </div>
  101. </div>
  102. ),
  103. notFoundComponent: () => (
  104. <div className="flex min-h-screen items-center justify-center p-6 text-center">
  105. <div>
  106. <h1 className="text-2xl font-bold">Film nicht gefunden</h1>
  107. <Link to="/" className="mt-4 inline-block text-primary underline">Zur Startseite</Link>
  108. </div>
  109. </div>
  110. ),
  111. component: MoviePage,
  112. });
  113.  
  114. function formatCurrency(n: number) {
  115. if (!n) return "—";
  116. return new Intl.NumberFormat("de-DE", { style: "currency", currency: "USD", maximumFractionDigits: 0 }).format(n);
  117. }
  118.  
  119. function formatDate(d: string) {
  120. if (!d) return "—";
  121. return new Intl.DateTimeFormat("de-DE", { day: "2-digit", month: "long", year: "numeric" }).format(new Date(d));
  122. }
  123.  
  124. function formatRuntime(min: number | null) {
  125. if (!min) return "—";
  126. const h = Math.floor(min / 60);
  127. const m = min % 60;
  128. return `${h} Std. ${m} Min.`;
  129. }
  130.  
  131. const STATUS_DE: Record<string, string> = {
  132. Released: "Veröffentlicht",
  133. "Post Production": "Post-Produktion",
  134. "In Production": "In Produktion",
  135. Planned: "Geplant",
  136. Rumored: "Gerücht",
  137. Canceled: "Abgesagt",
  138. };
  139.  
  140. function MoviePage() {
  141. const { details: m, credits, videos, recommendations } = Route.useLoaderData() as MovieBundle;
  142.  
  143. const backdrop = m.backdrop_path ? `${IMG_BASE}/original${m.backdrop_path}` : null;
  144. const poster = m.poster_path ? `${IMG_BASE}/w500${m.poster_path}` : null;
  145. const trailer = videos.results.find(
  146. (v) => v.site === "YouTube" && v.type === "Trailer" && v.official
  147. ) || videos.results.find((v) => v.site === "YouTube" && v.type === "Trailer")
  148. || videos.results.find((v) => v.site === "YouTube");
  149.  
  150. const directors = credits.crew.filter((c) => c.job === "Director");
  151. const writers = credits.crew.filter((c) =>
  152. ["Screenplay", "Writer", "Story", "Author"].includes(c.job)
  153. );
  154. const cast = credits.cast.slice(0, 10);
  155. const recs = recommendations.results.slice(0, 6);
  156.  
  157. return (
  158. <div className="min-h-screen bg-background pb-20">
  159. {/* Hero */}
  160. <section className="relative isolate overflow-hidden">
  161. {backdrop && (
  162. <img
  163. src={backdrop}
  164. alt=""
  165. loading="eager"
  166. className="absolute inset-0 h-full w-full object-cover opacity-30"
  167. />
  168. )}
  169. <div className="absolute inset-0 bg-gradient-to-b from-background/60 via-background/80 to-background" />
  170. <div className="relative mx-auto grid max-w-6xl gap-8 px-4 py-16 md:grid-cols-[280px_1fr] md:py-24">
  171. {poster && (
  172. <img
  173. src={poster}
  174. alt={`Poster von ${m.title}`}
  175. className="mx-auto w-56 rounded-2xl shadow-2xl md:w-full"
  176. loading="eager"
  177. />
  178. )}
  179. <div>
  180. <nav aria-label="Breadcrumb" className="mb-3 text-xs text-muted-foreground">
  181. <Link to="/" className="hover:underline">Startseite</Link>
  182. <span className="mx-2">/</span>
  183. <span>{m.title}</span>
  184. </nav>
  185. <h1 className="text-3xl font-bold tracking-tight text-foreground sm:text-4xl md:text-5xl">
  186. {m.title}
  187. </h1>
  188. {m.original_title !== m.title && (
  189. <p className="mt-1 text-sm text-muted-foreground">
  190. Originaltitel: {m.original_title}
  191. </p>
  192. )}
  193. {m.tagline && <p className="mt-3 italic text-muted-foreground">"{m.tagline}"</p>}
  194. <div className="mt-5 flex flex-wrap gap-2">
  195. {m.genres.map((g) => (
  196. <span
  197. key={g.id}
  198. className="rounded-full bg-secondary px-3 py-1 text-xs font-medium text-secondary-foreground"
  199. >
  200. {g.name}
  201. </span>
  202. ))}
  203. </div>
  204. <div className="mt-4 flex flex-wrap items-center gap-x-6 gap-y-2 text-sm text-muted-foreground">
  205. <span>⭐ {m.vote_average.toFixed(1)} / 10 ({m.vote_count.toLocaleString("de-DE")} Stimmen)</span>
  206. <span>{formatRuntime(m.runtime)}</span>
  207. <span>{formatDate(m.release_date)}</span>
  208. </div>
  209. <a
  210. href={MOVIE_CONFIG.watchUrl}
  211. target="_blank"
  212. rel="noopener noreferrer"
  213. 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"
  214. >
  215. ▶ {MOVIE_CONFIG.watchText}
  216. </a>
  217. </div>
  218. </div>
  219. </section>
  220.  
  221. <div className="mx-auto max-w-6xl space-y-8 px-4">
  222. {/* Ad: leaderboard (728x90 desktop, 320x50 mobile) */}
  223. <div className="hidden justify-center md:flex">
  224. <AtOptionsAd adKey="add579d28cd593077da0cb857a4cab32" width={728} height={90} />
  225. </div>
  226. <div className="flex justify-center md:hidden">
  227. <AtOptionsAd adKey="b6529f582f191a94d4ed6d9700cbbfba" width={320} height={50} />
  228. </div>
  229.  
  230. {/* Synopsis */}
  231. <Card title="Handlung">
  232. <p className="leading-relaxed text-foreground/90">{m.overview || "Keine Beschreibung verfügbar."}</p>
  233. </Card>
  234.  
  235. {/* Trailer */}
  236. {trailer && (
  237. <Card title="Offizieller Trailer">
  238. <div className="relative aspect-video w-full overflow-hidden rounded-xl bg-black">
  239. <iframe
  240. src={`https://www.youtube-nocookie.com/embed/${trailer.key}`}
  241. title={`Trailer: ${m.title}`}
  242. loading="lazy"
  243. allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
  244. allowFullScreen
  245. className="absolute inset-0 h-full w-full"
  246. />
  247. </div>
  248. </Card>
  249. )}
  250.  
  251. {/* Ad: native banner + 468x60 */}
  252. <NativeBannerAd />
  253. <div className="hidden justify-center sm:flex">
  254. <AtOptionsAd adKey="5da568282f66fb9805ae7e33c2d98d6f" width={468} height={60} />
  255. </div>
  256.  
  257. {/* Cast */}
  258. {cast.length > 0 && (
  259. <Card title="Besetzung">
  260. <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-5">
  261. {cast.map((c) => (
  262. <div key={c.id} className="text-center">
  263. <div className="mx-auto aspect-[2/3] w-full overflow-hidden rounded-lg bg-muted">
  264. {c.profile_path ? (
  265. <img
  266. src={`${IMG_BASE}/w185${c.profile_path}`}
  267. alt={c.name}
  268. loading="lazy"
  269. className="h-full w-full object-cover"
  270. />
  271. ) : (
  272. <div className="flex h-full items-center justify-center text-xs text-muted-foreground">
  273. Kein Bild
  274. </div>
  275. )}
  276. </div>
  277. <p className="mt-2 text-sm font-medium text-foreground">{c.name}</p>
  278. <p className="text-xs text-muted-foreground">{c.character}</p>
  279. </div>
  280. ))}
  281. </div>
  282. </Card>
  283. )}
  284.  
  285. {/* Directors / Writers */}
  286. <div className="grid gap-8 md:grid-cols-2">
  287. <Card title="Regie">
  288. {directors.length ? (
  289. <ul className="space-y-1">
  290. {directors.map((d) => (
  291. <li key={d.id} className="text-foreground">{d.name}</li>
  292. ))}
  293. </ul>
  294. ) : (
  295. <p className="text-muted-foreground">—</p>
  296. )}
  297. </Card>
  298. <Card title="Drehbuch">
  299. {writers.length ? (
  300. <ul className="space-y-1">
  301. {writers.map((w) => (
  302. <li key={`${w.id}-${w.job}`} className="text-foreground">
  303. {w.name} <span className="text-xs text-muted-foreground">({w.job})</span>
  304. </li>
  305. ))}
  306. </ul>
  307. ) : (
  308. <p className="text-muted-foreground">—</p>
  309. )}
  310. </Card>
  311. </div>
  312.  
  313. {/* Facts */}
  314. <Card title="Film-Fakten">
  315. <dl className="grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3">
  316. <Fact label="Status" value={STATUS_DE[m.status] ?? m.status} />
  317. <Fact label="Originalsprache" value={m.original_language?.toUpperCase()} />
  318. <Fact label="Veröffentlichung" value={formatDate(m.release_date)} />
  319. <Fact label="Laufzeit" value={formatRuntime(m.runtime)} />
  320. <Fact label="Budget" value={formatCurrency(m.budget)} />
  321. <Fact label="Einspielergebnis" value={formatCurrency(m.revenue)} />
  322. <Fact label="TMDb-Bewertung" value={`${m.vote_average.toFixed(1)} / 10`} />
  323. <Fact label="Anzahl Stimmen" value={m.vote_count.toLocaleString("de-DE")} />
  324. <Fact
  325. label="Gesprochene Sprachen"
  326. value={m.spoken_languages.map((s) => s.name).join(", ") || "—"}
  327. />
  328. </dl>
  329. </Card>
  330.  
  331. {/* Production */}
  332. <Card title="Produktion">
  333. <div className="space-y-4">
  334. <div>
  335. <h3 className="text-sm font-medium text-muted-foreground">Produktionsfirmen</h3>
  336. <p className="mt-1 text-foreground">
  337. {m.production_companies.map((c) => c.name).join(", ") || "—"}
  338. </p>
  339. </div>
  340. <div>
  341. <h3 className="text-sm font-medium text-muted-foreground">Produktionsländer</h3>
  342. <p className="mt-1 text-foreground">
  343. {m.production_countries.map((c) => c.name).join(", ") || "—"}
  344. </p>
  345. </div>
  346. </div>
  347. </Card>
  348.  
  349. {/* Recommendations */}
  350. {recs.length > 0 && (
  351. <Card title="Ähnliche Filme">
  352. <div className="grid grid-cols-2 gap-4 sm:grid-cols-3 md:grid-cols-6">
  353. {recs.map((r) => (
  354. <Link
  355. key={r.id}
  356. to="/movie/$movieSlug"
  357. params={{ movieSlug: `${r.id}-${MOVIE_CONFIG.slug}` }}
  358. className="group"
  359. >
  360. <div className="aspect-[2/3] overflow-hidden rounded-lg bg-muted">
  361. {r.poster_path ? (
  362. <img
  363. src={`${IMG_BASE}/w342${r.poster_path}`}
  364. alt={r.title}
  365. loading="lazy"
  366. className="h-full w-full object-cover transition-transform group-hover:scale-105"
  367. />
  368. ) : null}
  369. </div>
  370. <p className="mt-2 line-clamp-2 text-sm font-medium text-foreground">{r.title}</p>
  371. <p className="text-xs text-muted-foreground">
  372. {r.release_date?.slice(0, 4) || ""}
  373. </p>
  374. </Link>
  375. ))}
  376. </div>
  377. </Card>
  378. )}
  379.  
  380. {/* Ad: 160x600 skyscraper (centered on desktop) */}
  381. <div className="hidden justify-center lg:flex">
  382. <AtOptionsAd adKey="00de6b5d88ffd4850a38f045cca6bb12" width={160} height={600} />
  383. </div>
  384.  
  385. {/* Big watch button */}
  386. <div className="pt-6 text-center">
  387. <a
  388. href={MOVIE_CONFIG.watchUrl}
  389. target="_blank"
  390. rel="noopener noreferrer"
  391. 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"
  392. >
  393. ▶ {MOVIE_CONFIG.watchText}
  394. </a>
  395. </div>
  396. </div>
  397. </div>
  398. );
  399. }
  400.  
  401. function Card({ title, children }: { title: string; children: React.ReactNode }) {
  402. return (
  403. <section className="rounded-2xl border border-border bg-card p-6 shadow-sm md:p-8">
  404. <h2 className="mb-4 text-xl font-semibold text-card-foreground md:text-2xl">{title}</h2>
  405. {children}
  406. </section>
  407. );
  408. }
  409.  
  410. function Fact({ label, value }: { label: string; value: string }) {
  411. return (
  412. <div>
  413. <dt className="text-xs font-medium uppercase tracking-wide text-muted-foreground">{label}</dt>
  414. <dd className="mt-1 text-foreground">{value}</dd>
  415. </div>
  416. );
  417. }
  418.  
Advertisement
Add Comment
Please, Sign In to add comment