Skip to content

Commit ad0f3a9

Browse files
committed
Merge branch 'pr-43'
2 parents 887998c + 08f6038 commit ad0f3a9

40 files changed

+2163
-380
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { ReactNode } from 'react';
2+
import type { Metadata } from 'next';
3+
import { db } from '@snow-leopard/db';
4+
import * as schema from '@snow-leopard/db';
5+
import { eq, and } from 'drizzle-orm';
6+
7+
export async function generateMetadata({ params }: any) {
8+
const { author, slug } = params;
9+
const result = await db
10+
.select()
11+
.from(schema.Document)
12+
.where(
13+
and(
14+
eq(schema.Document.author, author),
15+
eq(schema.Document.slug, slug),
16+
eq(schema.Document.visibility, 'public')
17+
)
18+
)
19+
.limit(1);
20+
const doc = result[0];
21+
if (!doc) {
22+
return { title: 'Snow Leopard' };
23+
}
24+
const dateString = new Date(doc.createdAt).toLocaleDateString('en-US');
25+
const title = doc.title;
26+
const description = (doc.content ?? '').slice(0, 160);
27+
const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || '';
28+
const ogUrl = `${baseUrl}/api/og?type=post&title=${encodeURIComponent(
29+
title
30+
)}&author=${encodeURIComponent(author)}&date=${encodeURIComponent(dateString)}`;
31+
return {
32+
title,
33+
description,
34+
openGraph: {
35+
url: `${baseUrl}/${author}/${slug}`,
36+
title,
37+
description,
38+
siteName: 'snowleopard',
39+
images: [
40+
{ url: ogUrl, width: 1200, height: 630, alt: title },
41+
],
42+
},
43+
twitter: {
44+
card: 'summary_large_image',
45+
title,
46+
description,
47+
images: [{ url: ogUrl, alt: title }],
48+
},
49+
};
50+
}
51+
52+
export default function BlogPageLayout({ children }: { children: ReactNode }) {
53+
return <>{children}</>;
54+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { notFound } from 'next/navigation';
2+
import { db } from '@snow-leopard/db';
3+
import * as schema from '@snow-leopard/db';
4+
import { eq, and } from 'drizzle-orm';
5+
import { Blog } from '@/components/blog';
6+
import AIChatWidget from '@/components/ai-chat-widget';
7+
import ThemeToggle from '@/components/theme-toggle';
8+
import Link from 'next/link';
9+
import { Button } from '@/components/ui/button';
10+
11+
export default async function Page({ params }: any) {
12+
const { author, slug } = await params;
13+
const result = await db
14+
.select()
15+
.from(schema.Document)
16+
.where(
17+
and(
18+
eq(schema.Document.author, author),
19+
eq(schema.Document.slug, slug),
20+
eq(schema.Document.visibility, 'public')
21+
)
22+
)
23+
.limit(1);
24+
const doc = result[0];
25+
if (!doc) {
26+
notFound();
27+
}
28+
const styleObj = (doc.style as any) || {};
29+
const font = styleObj.font as any;
30+
const accentColor = styleObj.accentColor as string;
31+
const textColorLight = styleObj.textColorLight as string;
32+
const textColorDark = styleObj.textColorDark as string;
33+
const dateString = new Date(doc.createdAt).toLocaleDateString('en-US');
34+
return (
35+
<>
36+
<ThemeToggle />
37+
<Link href="/register">
38+
<Button variant="outline" className="fixed top-4 right-4 z-50">
39+
Sign up to Snow Leopard
40+
</Button>
41+
</Link>
42+
<Blog
43+
title={doc.title}
44+
content={doc.content || ''}
45+
font={font}
46+
accentColor={accentColor}
47+
textColorLight={textColorLight}
48+
textColorDark={textColorDark}
49+
author={doc.author || author}
50+
date={dateString}
51+
/>
52+
<AIChatWidget
53+
context={doc.content || ''}
54+
title={doc.title}
55+
author={doc.author || author}
56+
date={dateString}
57+
/>
58+
</>
59+
);
60+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import { streamText, smoothStream } from 'ai';
2+
import { DEFAULT_CHAT_MODEL } from '@/lib/ai/models';
3+
import { myProvider } from '@/lib/ai/providers';
4+
5+
export async function POST(request: Request) {
6+
const { messages, context, title, author, date } = await request.json();
7+
8+
const prompt = [
9+
'You are a helpful AI assistant answering reader questions about the following article. Provide concise, accurate answers.',
10+
'',
11+
`Title: ${title}`,
12+
`Author: ${author}`,
13+
`Date: ${date}`,
14+
'',
15+
context,
16+
...messages.map((m: any) => `${m.role}: ${m.content}`),
17+
'assistant:'
18+
].join('\n');
19+
20+
const { fullStream } = streamText({
21+
model: myProvider.languageModel(DEFAULT_CHAT_MODEL),
22+
prompt,
23+
experimental_transform: smoothStream({ chunking: 'word' }),
24+
});
25+
26+
return new Response(
27+
new ReadableStream({
28+
async start(controller) {
29+
for await (const delta of fullStream) {
30+
if (delta.type === 'text-delta') {
31+
controller.enqueue(new TextEncoder().encode(delta.textDelta));
32+
}
33+
}
34+
controller.close();
35+
}
36+
}),
37+
{ headers: { 'Content-Type': 'text/plain; charset=utf-8' } }
38+
);
39+
}

apps/snow-leopard/app/api/chat/route.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ import { NextResponse } from 'next/server';
2929
import { myProvider } from '@/lib/ai/providers';
3030
import { auth } from "@/lib/auth";
3131
import { headers } from 'next/headers';
32-
import { ArtifactKind } from '@/components/artifact';
3332
import type { Document } from '@snow-leopard/db';
3433
import { createDocument as aiCreateDocument } from '@/lib/ai/tools/create-document';
3534
import { webSearch } from '@/lib/ai/tools/web-search';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { auth } from "@/lib/auth";
3+
import { headers } from "next/headers";
4+
import { updateDocumentPublishSettings, getActiveSubscriptionByUserId } from "@/lib/db/queries";
5+
6+
export async function publishDocument(request: NextRequest, body: any): Promise<NextResponse> {
7+
const readonlyHeaders = await headers();
8+
const requestHeaders = new Headers(readonlyHeaders);
9+
const session = await auth.api.getSession({ headers: requestHeaders });
10+
if (!session?.user?.id) {
11+
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
12+
}
13+
const userId = session.user.id;
14+
15+
// Server-side subscription check
16+
if (process.env.STRIPE_ENABLED === 'true') {
17+
const subscription = await getActiveSubscriptionByUserId({ userId });
18+
if (!subscription || subscription.status !== 'active') {
19+
return NextResponse.json({ error: 'Payment Required: publishing is pro-only' }, { status: 402 });
20+
}
21+
}
22+
23+
const { id: documentId, visibility, author, style, slug } = body;
24+
if (!documentId || !slug) {
25+
return NextResponse.json({ error: 'Invalid parameters' }, { status: 400 });
26+
}
27+
28+
try {
29+
const updatedDocument = await updateDocumentPublishSettings({ documentId, userId, visibility, author, style, slug });
30+
return NextResponse.json(updatedDocument);
31+
} catch (error: any) {
32+
console.error('[API /document/publish] Failed to update publish settings:', error);
33+
if (typeof error?.message === 'string' && error.message.toLowerCase().includes('already published')) {
34+
return NextResponse.json({ error: error.message }, { status: 409 });
35+
}
36+
return NextResponse.json({ error: error.message || 'Failed to update publish settings' }, { status: 500 });
37+
}
38+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
import { publishDocument } from '../actions/publish';
3+
4+
export async function POST(request: NextRequest) {
5+
let body: any;
6+
try {
7+
body = await request.json();
8+
} catch (error: any) {
9+
console.error('[API /document/publish] Invalid JSON:', error);
10+
return NextResponse.json({ error: 'Invalid JSON body' }, { status: 400 });
11+
}
12+
try {
13+
return await publishDocument(request, body);
14+
} catch (error: any) {
15+
console.error('[API /document/publish] Error handling publish:', error);
16+
return NextResponse.json({ error: error.message || 'Error publishing document' }, { status: 500 });
17+
}
18+
}

apps/snow-leopard/app/api/og/route.tsx

Lines changed: 105 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -5,68 +5,124 @@ export const runtime = 'edge';
55

66
export async function GET(req: NextRequest) {
77
const { searchParams } = new URL(req.url);
8+
const type = searchParams.get('type');
89

9-
// ?title=<title>
10-
const hasTitle = searchParams.has('title');
11-
const title = hasTitle
12-
? searchParams.get('title')?.slice(0, 100)
13-
: 'Snow Leopard';
10+
if (
11+
type === 'post' &&
12+
searchParams.has('title') &&
13+
searchParams.has('author') &&
14+
searchParams.has('date')
15+
) {
16+
const title = searchParams.get('title')?.slice(0, 100) ?? 'Untitled';
17+
const author = searchParams.get('author')?.slice(0, 50) ?? 'Anonymous';
18+
const date = searchParams.get('date') ?? '';
19+
20+
return new ImageResponse(
21+
(
22+
<div
23+
style={{
24+
width: '100%',
25+
height: '100%',
26+
display: 'flex',
27+
flexDirection: 'column',
28+
background: 'white',
29+
padding: '60px',
30+
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
31+
position: 'relative',
32+
}}
33+
>
34+
<div
35+
style={{
36+
position: 'absolute',
37+
top: 60,
38+
left: 60,
39+
fontSize: 28,
40+
fontWeight: 500,
41+
color: '#1f2937',
42+
}}
43+
>
44+
snow leopard
45+
</div>
46+
<div
47+
style={{
48+
display: 'flex',
49+
flexDirection: 'column',
50+
justifyContent: 'center',
51+
alignItems: 'center',
52+
flexGrow: 1,
53+
paddingTop: 80,
54+
}}
55+
>
56+
<div
57+
style={{
58+
fontSize: 72,
59+
fontWeight: 800,
60+
color: '#1f2937',
61+
lineHeight: 1.1,
62+
maxWidth: '95%',
63+
textAlign: 'center',
64+
}}
65+
>
66+
{title}
67+
</div>
68+
<div
69+
style={{
70+
display: 'flex',
71+
alignItems: 'center',
72+
marginTop: 32,
73+
fontSize: 32,
74+
fontWeight: 400,
75+
color: '#6b7280',
76+
}}
77+
>
78+
<span>{author}</span>
79+
<span style={{ margin: '0 12px' }}></span>
80+
<span>{date}</span>
81+
</div>
82+
</div>
83+
</div>
84+
),
85+
{ width: 1200, height: 630 }
86+
);
87+
}
1488

1589
return new ImageResponse(
1690
(
17-
<div style={{
18-
width: 1200,
19-
height: 630,
20-
display: 'flex',
21-
alignItems: 'center',
22-
justifyContent: 'center',
23-
background: 'radial-gradient(circle at center, #ffffff 60%, #f0f0f0 100%)', // floating backdrop
24-
}}>
25-
<div style={{
26-
width: 600,
27-
height: 600,
28-
borderRadius: '50%',
29-
background: 'radial-gradient(circle at center, #f9f9f9 0%, #e0e0e0 100%)',
30-
border: '8px solid #c0c0c0', // silver border
31-
boxShadow: '0 20px 40px rgba(0,0,0,0.15), inset 0 0 30px rgba(0,0,0,0.03)', // drop shadow for floating effect
32-
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
33-
position: 'relative',
91+
<div
92+
style={{
93+
width: '100%',
94+
height: '100%',
3495
display: 'flex',
3596
flexDirection: 'column',
3697
alignItems: 'center',
3798
justifyContent: 'center',
38-
}}>
39-
<div style={{
40-
fontSize: 72,
41-
fontWeight: 800,
99+
background: 'white',
100+
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
101+
padding: '60px',
102+
}}
103+
>
104+
<div
105+
style={{
106+
fontSize: 84,
107+
fontWeight: 500,
42108
letterSpacing: '-0.05em',
43-
lineHeight: 1,
44-
textAlign: 'center',
45-
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
46-
color: '#1f2937', // gray-800
47-
}}>
48-
{title}
49-
</div>
50-
<div style={{
51-
fontSize: 28,
52-
fontWeight: 400,
109+
color: '#1f2937',
110+
}}
111+
>
112+
Snow Leopard
113+
</div>
114+
<div
115+
style={{
116+
fontSize: 32,
53117
marginTop: 24,
54-
color: '#6b7280', // gray-500
55-
textAlign: 'center',
56-
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
57-
}}>
58-
Tab, Tab, Apply
59-
</div>
60-
<div style={{
61-
fontSize: 28,
62118
fontWeight: 400,
63-
marginTop: 8,
64119
color: '#6b7280',
65120
textAlign: 'center',
66-
fontFamily: '"SF Pro Display", "Helvetica Neue", "Arial", sans-serif',
67-
}}>
68-
Brilliance
69-
</div>
121+
maxWidth: '75%',
122+
lineHeight: 1.4,
123+
}}
124+
>
125+
The most satisfying, intuitive AI writing tool, and it&apos;s open source.
70126
</div>
71127
</div>
72128
),

0 commit comments

Comments
 (0)