Skip to content

Commit 01d0fb1

Browse files
committed
new preview links working
1 parent 67efd9f commit 01d0fb1

8 files changed

Lines changed: 221 additions & 130 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import type { PortableTextBlock } from "next-sanity";
2+
import { Suspense } from "react";
3+
4+
import DateComponent from "@/components/date";
5+
import MoreContent from "@/components/more-content";
6+
import PortableText from "@/components/portable-text";
7+
8+
import type { PodcastQueryResult } from "@/sanity/types";
9+
import CoverMedia from "@/components/cover-media";
10+
import MoreHeader from "@/components/more-header";
11+
import { BreadcrumbLinks } from "@/components/breadrumb-links";
12+
import SponsorCard from "@/components/sponsor-card";
13+
import Avatar from "@/components/avatar";
14+
import Picks from "./picks";
15+
import PlayerPlayButton from "@/components/player-play-button";
16+
import PodcastOpenSpotify from "@/components/podcast-open-spotify";
17+
import PodcastOpenApple from "@/components/podcast-open-apple";
18+
import PodcastOpenYouTube from "@/components/podcast-open-youtube";
19+
import CarbonAdBanner from "@/components/carbon-ad-banner";
20+
21+
export default async function Podcast({ podcast }: { podcast: NonNullable<PodcastQueryResult> }) {
22+
23+
24+
const src = podcast?.spotify?.enclosures?.at(0)?.url;
25+
26+
return (
27+
<div className="container px-5 mx-auto">
28+
<BreadcrumbLinks
29+
links={[{ title: "Podcasts", href: "/podcasts/page/1" }]}
30+
/>
31+
<article>
32+
<h1 className="mb-12 text-4xl font-bold leading-tight tracking-tighter text-balance md:text-7xl md:leading-none lg:text-8xl">
33+
{podcast.title}
34+
</h1>
35+
36+
<div className="mb-8 sm:mx-0 md:mb-16">
37+
<CoverMedia
38+
cloudinaryImage={podcast?.coverImage}
39+
cloudinaryVideo={podcast?.videoCloudinary}
40+
youtube={podcast?.youtube}
41+
/>
42+
</div>
43+
44+
<div className="max-w-2xl sm:max-w-none">
45+
<div className="flex flex-wrap justify-between">
46+
<div className="flex-1">
47+
<div className="mb-6">
48+
{(podcast?.author || podcast?.guest) && (
49+
<div className="flex flex-wrap gap-2">
50+
{podcast?.author?.map((a, idx) => (
51+
<Avatar
52+
key={a._id || `author-${idx}`}
53+
name={a.title}
54+
href={`/author/${a?.slug}`}
55+
coverImage={a?.coverImage}
56+
/>
57+
))}
58+
{podcast?.guest?.map((a, idx) => (
59+
<Avatar
60+
key={a._id || `guest-${idx}`}
61+
name={a.title}
62+
href={`/guest/${a?.slug}`}
63+
coverImage={a?.coverImage}
64+
/>
65+
))}
66+
</div>
67+
)}
68+
</div>
69+
<div className="mb-4 text-lg">
70+
<DateComponent dateString={podcast.date} />
71+
</div>
72+
</div>
73+
<CarbonAdBanner />
74+
</div>
75+
76+
{src && (
77+
<div className="flex flex-col sm:flex-row justify-start flex-wrap w-full p-2 sm:p-4 border border-foreground gap-2 sm:gap-4 items-center">
78+
<h2 className="text-xl font-bold w-full text-center sm:text-start">
79+
Listening Options
80+
</h2>
81+
<PlayerPlayButton podcast={podcast} />
82+
<span className="text-xl">or</span>
83+
<div className="flex gap-1">
84+
<PodcastOpenSpotify podcast={podcast} />
85+
<PodcastOpenApple />
86+
<PodcastOpenYouTube podcast={podcast} />
87+
</div>
88+
</div>
89+
)}
90+
</div>
91+
{podcast?.sponsor?.length && (
92+
<section className="flex flex-col mt-10 mb-10">
93+
<h2 className="mb-4 text-2xl font-bold">Sponsors</h2>
94+
<hr className="border-accent-2" />
95+
<div className="my-12 ">
96+
<SponsorCard sponsors={podcast.sponsor} />
97+
</div>
98+
<hr className="border-accent-2" />
99+
</section>
100+
)}
101+
102+
{podcast?.content?.length && (
103+
<PortableText
104+
className="mx-auto prose-violet lg:prose-xl dark:prose-invert"
105+
value={podcast.content as PortableTextBlock[]}
106+
/>
107+
)}
108+
</article>
109+
{podcast?.pick?.length && (
110+
<>
111+
<hr className="mb-8 sm:mb-24 border-accent-2 mt-8 sm:mt-28" />
112+
<div className="flex flex-col md:flex-row md:justify-between">
113+
<h2 className="mb-8 text-6xl font-bold leading-tight tracking-tighter md:text-7xl">
114+
Picks
115+
</h2>
116+
</div>
117+
118+
<section className="p-0 sm:p-12">
119+
<div className="grid gap-2 sm:gap-8">
120+
<Picks picks={podcast.pick} />
121+
</div>
122+
</section>
123+
</>
124+
)}
125+
<aside>
126+
<MoreHeader title="Recent Podcasts" href="/podcasts/page/1" />
127+
<Suspense>
128+
<MoreContent type={podcast._type} skip={podcast._id} limit={2} />
129+
</Suspense>
130+
</aside>
131+
</div>
132+
);
133+
}
Lines changed: 2 additions & 125 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,10 @@
11
import type { Metadata, ResolvingMetadata } from "next";
2-
import type { PortableTextBlock } from "next-sanity";
32
import { notFound } from "next/navigation";
4-
import { Suspense } from "react";
5-
6-
import DateComponent from "@/components/date";
7-
import MoreContent from "@/components/more-content";
8-
import PortableText from "@/components/portable-text";
9-
103
import type { PodcastQueryResult } from "@/sanity/types";
114
import { sanityFetch } from "@/sanity/lib/live";
125
import { podcastQuery } from "@/sanity/lib/queries";
136
import { resolveOpenGraphImage } from "@/sanity/lib/utils";
14-
import CoverMedia from "@/components/cover-media";
15-
import MoreHeader from "@/components/more-header";
16-
import { BreadcrumbLinks } from "@/components/breadrumb-links";
17-
import SponsorCard from "@/components/sponsor-card";
18-
import Avatar from "@/components/avatar";
19-
import Picks from "./picks";
20-
import PlayerPlayButton from "@/components/player-play-button";
21-
import PodcastOpenSpotify from "@/components/podcast-open-spotify";
22-
import PodcastOpenApple from "@/components/podcast-open-apple";
23-
import PodcastOpenYouTube from "@/components/podcast-open-youtube";
24-
import CarbonAdBanner from "@/components/carbon-ad-banner";
7+
import Podcast from "../Podcast";
258

269
type Params = Promise<{ slug: string }>;
2710

@@ -70,113 +53,7 @@ export default async function PodcastPage({ params }: { params: Params }) {
7053
return notFound();
7154
}
7255

73-
const src = podcast?.spotify?.enclosures?.at(0)?.url;
74-
7556
return (
76-
<div className="container px-5 mx-auto">
77-
<BreadcrumbLinks
78-
links={[{ title: "Podcasts", href: "/podcasts/page/1" }]}
79-
/>
80-
<article>
81-
<h1 className="mb-12 text-4xl font-bold leading-tight tracking-tighter text-balance md:text-7xl md:leading-none lg:text-8xl">
82-
{podcast.title}
83-
</h1>
84-
85-
<div className="mb-8 sm:mx-0 md:mb-16">
86-
<CoverMedia
87-
cloudinaryImage={podcast?.coverImage}
88-
cloudinaryVideo={podcast?.videoCloudinary}
89-
youtube={podcast?.youtube}
90-
/>
91-
</div>
92-
93-
<div className="max-w-2xl sm:max-w-none">
94-
<div className="flex flex-wrap justify-between">
95-
<div className="flex-1">
96-
<div className="mb-6">
97-
{(podcast?.author || podcast?.guest) && (
98-
<div className="flex flex-wrap gap-2">
99-
{podcast?.author?.map((a) => (
100-
<Avatar
101-
key={a._id}
102-
name={a.title}
103-
href={`/author/${a?.slug}`}
104-
coverImage={a?.coverImage}
105-
/>
106-
))}
107-
{podcast?.guest?.map((a) => (
108-
<Avatar
109-
key={a._id}
110-
name={a.title}
111-
href={`/guest/${a?.slug}`}
112-
coverImage={a?.coverImage}
113-
/>
114-
))}
115-
</div>
116-
)}
117-
</div>
118-
<div className="mb-4 text-lg">
119-
<DateComponent dateString={podcast.date} />
120-
</div>
121-
</div>
122-
<CarbonAdBanner />
123-
</div>
124-
125-
{src && (
126-
<div className="flex flex-col sm:flex-row justify-start flex-wrap w-full p-2 sm:p-4 border border-foreground gap-2 sm:gap-4 items-center">
127-
<h2 className="text-xl font-bold w-full text-center sm:text-start">
128-
Listening Options
129-
</h2>
130-
<PlayerPlayButton podcast={podcast} />
131-
<span className="text-xl">or</span>
132-
<div className="flex gap-1">
133-
<PodcastOpenSpotify podcast={podcast} />
134-
<PodcastOpenApple />
135-
<PodcastOpenYouTube podcast={podcast} />
136-
</div>
137-
</div>
138-
)}
139-
</div>
140-
{podcast?.sponsor?.length && (
141-
<section className="flex flex-col mt-10 mb-10">
142-
<h2 className="mb-4 text-2xl font-bold">Sponsors</h2>
143-
<hr className="border-accent-2" />
144-
<div className="my-12 ">
145-
<SponsorCard sponsors={podcast.sponsor} />
146-
</div>
147-
<hr className="border-accent-2" />
148-
</section>
149-
)}
150-
151-
{podcast?.content?.length && (
152-
<PortableText
153-
className="mx-auto prose-violet lg:prose-xl dark:prose-invert"
154-
value={podcast.content as PortableTextBlock[]}
155-
/>
156-
)}
157-
</article>
158-
{podcast?.pick?.length && (
159-
<>
160-
<hr className="mb-8 sm:mb-24 border-accent-2 mt-8 sm:mt-28" />
161-
<div className="flex flex-col md:flex-row md:justify-between">
162-
<h2 className="mb-8 text-6xl font-bold leading-tight tracking-tighter md:text-7xl">
163-
Picks
164-
</h2>
165-
</div>
166-
167-
<section className="p-0 sm:p-12">
168-
<div className="grid gap-2 sm:gap-8">
169-
<Picks picks={podcast.pick} />
170-
</div>
171-
</section>
172-
</>
173-
)}
174-
<aside>
175-
<MoreHeader title="Recent Podcasts" href="/podcasts/page/1" />
176-
<Suspense>
177-
<MoreContent type={podcast._type} skip={podcast._id} limit={2} />
178-
</Suspense>
179-
</aside>
180-
</div>
57+
<Podcast podcast={podcast} />
18158
);
18259
}
File renamed without changes.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
2+
3+
import { notFound } from 'next/navigation';
4+
import { headers } from 'next/headers';
5+
import Podcast from '../../Podcast';
6+
7+
export default async function PreviewPage({ params }: { params: Promise<{ token: string }> }) {
8+
const { token } = await params;
9+
if (!token) return notFound();
10+
11+
// Build absolute URL for API call
12+
const headersList = await headers();
13+
const host = headersList.get('host');
14+
const protocol = host?.startsWith('localhost') ? 'http' : 'https';
15+
const apiUrl = `${protocol}://${host}/api/get-preview-content`;
16+
17+
const res = await fetch(apiUrl, {
18+
method: 'POST',
19+
headers: { 'Content-Type': 'application/json' },
20+
body: JSON.stringify({ token }),
21+
cache: 'no-store',
22+
});
23+
24+
if (!res.ok) return notFound();
25+
const data = await res.json();
26+
27+
if (!data || !data.document) return notFound();
28+
29+
return (
30+
<Podcast podcast={data.document} />
31+
);
32+
}

app/api/generate-preview-token/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ export async function POST(req: NextRequest) {
2424
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
2525
}
2626
const token = uuidv4();
27-
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 2).toISOString(); // 2 hours expiry
27+
const expiresAt = new Date(Date.now() + 1000 * 60 * 60 * 24 * 7).toISOString(); // 7 days
2828

2929
// Create previewSession document in Sanity
3030
await sanityClient.create({

app/api/get-preview-content/route.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
import type { NextRequest } from 'next/server';
22
import { NextResponse } from 'next/server';
33
import { createClient } from 'next-sanity';
4+
import { podcastQuery, postQuery } from '@/sanity/lib/queries';
45

56
const sanityClient = createClient({
67
projectId: process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!,
78
dataset: process.env.NEXT_PUBLIC_SANITY_DATASET!,
89
apiVersion: '2025-09-22',
9-
token: process.env.SANITY_API_TOKEN, // must have read access
10+
token: process.env.SANITY_API_READ_TOKEN,
1011
useCdn: false,
12+
perspective: 'drafts',
1113
});
1214

1315
export async function POST(req: NextRequest) {
@@ -36,5 +38,17 @@ export async function POST(req: NextRequest) {
3638
return NextResponse.json({ error: 'Document not found' }, { status: 404 });
3739
}
3840

39-
return NextResponse.json({ document: doc });
41+
if (doc?._type !== 'post' && doc?._type !== 'podcast' && !doc.slug?.current) {
42+
return NextResponse.json({ error: 'Document not found' }, { status: 404 });
43+
}
44+
45+
if(doc?._type === 'podcast') {
46+
return NextResponse.json({ document: await sanityClient.fetch(podcastQuery, { slug: doc.slug.current }) });
47+
}
48+
49+
if(doc?._type === 'post') {
50+
return NextResponse.json({ document: await sanityClient.fetch(postQuery, { slug: doc.slug.current }) });
51+
}
52+
53+
return NextResponse.json({ error: 'Document not found' }, { status: 404 });
4054
}

app/preview/[token]/page.tsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
2+
3+
import { notFound } from 'next/navigation';
4+
import { headers } from 'next/headers';
5+
6+
export default async function PreviewPage({ params }: { params: Promise<{ token: string }> }) {
7+
const { token } = await params;
8+
if (!token) return notFound();
9+
10+
// Build absolute URL for API call
11+
const headersList = await headers();
12+
const host = headersList.get('host');
13+
const protocol = host?.startsWith('localhost') ? 'http' : 'https';
14+
const apiUrl = `${protocol}://${host}/api/get-preview-content`;
15+
16+
const res = await fetch(apiUrl, {
17+
method: 'POST',
18+
headers: { 'Content-Type': 'application/json' },
19+
body: JSON.stringify({ token }),
20+
cache: 'no-store',
21+
});
22+
23+
if (!res.ok) return notFound();
24+
const data = await res.json();
25+
26+
if (!data || !data.document) return notFound();
27+
28+
return (
29+
<main style={{ padding: 32 }}>
30+
<h1>Preview: {data.document.title || data.document._id}</h1>
31+
<pre style={{ background: '#f5f5f5', padding: 16, borderRadius: 8 }}>
32+
{JSON.stringify(data.document, null, 2)}
33+
</pre>
34+
</main>
35+
);
36+
}

0 commit comments

Comments
 (0)