Skip to content

Commit 2aa792f

Browse files
Saving Template to DB
1 parent ab26764 commit 2aa792f

File tree

19 files changed

+339
-155
lines changed

19 files changed

+339
-155
lines changed

bun.lockb

15 KB
Binary file not shown.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"lint": "next lint"
1111
},
1212
"dependencies": {
13-
"@clerk/nextjs": "^6.9.5",
13+
"@clerk/nextjs": "^6.12.4",
1414
"@google/generative-ai": "^0.22.0",
1515
"@heroui/breadcrumbs": "^2.2.11",
1616
"@heroui/drawer": "^2.2.12",
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[automationId]` on the table `Template` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- CreateIndex
8+
CREATE UNIQUE INDEX "Template_automationId_key" ON "Template"("automationId");
9+
10+
-- CreateIndex
11+
CREATE INDEX "Template_userId_automationId_idx" ON "Template"("userId", "automationId");
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
-- DropIndex
2+
DROP INDEX "Template_userId_automationId_idx";

prisma/schema.prisma

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ model User {
2626
notification Notification[]
2727
analytics Analytics[]
2828
openAiKey String? @unique
29-
templates Template[]
29+
templates Template[] // A user can have many templates (through different automations)
3030
}
3131

3232
model Subscription {
@@ -62,7 +62,7 @@ model Automation {
6262
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
6363
userId String? @db.Uuid
6464
keywords Keyword[]
65-
templates Template[]
65+
template Template? // Changed from templates to template (singular)
6666
}
6767

6868
model Dms {
@@ -168,8 +168,8 @@ model Template {
168168
buttons Json
169169
createdAt DateTime @default(now())
170170
updatedAt DateTime @updatedAt
171-
User User? @relation(fields: [userId], references: [id], onDelete: Cascade)
172-
userId String @db.Uuid
173-
Automation Automation @relation(fields: [automationId], references: [id], onDelete: Cascade)
174-
automationId String @db.Uuid
171+
User User @relation(fields: [userId], references: [id], onDelete: Cascade)
172+
userId String @db.Uuid
173+
Automation Automation @relation(fields: [automationId], references: [id], onDelete: Cascade)
174+
automationId String @db.Uuid @unique
175175
}

src/actions/automations/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ export const updateAutomationName = async (
9292

9393
export const saveListener = async (
9494
automationId: string,
95-
listener: "SMARTAI" | "MESSAGE",
95+
listener: "SMARTAI" | "MESSAGE" | "GENERIC_TEMPLATE",
9696
prompt: string,
9797
reply?: string
9898
) => {

src/actions/automations/queries.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export const updateAutomation = async (
8383

8484
export const addListener = async (
8585
automationId: string,
86-
listener: "SMARTAI" | "MESSAGE",
86+
listener: "SMARTAI" | "MESSAGE" | "GENERIC_TEMPLATE",
8787
prompt: string,
8888
reply?: string
8989
) => {

src/app/(protected)/api/webhook/instagram/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -380,4 +380,4 @@ export async function POST(req: NextRequest) {
380380
{ status: 200 }
381381
);
382382
}
383-
}
383+
}

src/app/(protected)/dashboard/[slug]/automations/[id]/_components/generic-template.tsx

Lines changed: 74 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
// components/generics-template.tsx
21
import React, { useState, useEffect } from 'react';
32
import { Switch } from '@/components/ui/switch';
43
import { useToast } from '@/hooks/use-toast';
54
import { Dialog, DialogContent } from '@/components/ui/dialog';
65
import Image from 'next/image';
6+
import TemplatePreviewCard from '@/components/global/generic-template-card/template-preview-card';
7+
78

89
interface Button {
910
type: 'web_url' | 'postback';
@@ -26,12 +27,18 @@ export const GenericTemplate = ({
2627
}: GenericTemplateProps) => {
2728
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
2829
const [showForm, setShowForm] = useState(true);
29-
const [template, setTemplate] = useState({
30+
const [template, setTemplate] = useState<{
31+
title: string;
32+
subtitle: string;
33+
imageUrl: string;
34+
defaultAction: string;
35+
buttons: Button[];
36+
}>({
3037
title: '',
3138
subtitle: '',
3239
imageUrl: '',
3340
defaultAction: '',
34-
buttons: [] as Button[]
41+
buttons: [] // Ensure buttons is initialized as an empty array
3542
});
3643
const [isSaving, setIsSaving] = useState(false);
3744
const { toast } = useToast();
@@ -43,15 +50,6 @@ export const GenericTemplate = ({
4350

4451
const fetchTemplate = async () => {
4552
try {
46-
const cachedTemplate = sessionStorage.getItem(`template-${automationId}`);
47-
if (cachedTemplate) {
48-
const parsedTemplate = JSON.parse(cachedTemplate);
49-
setTemplate(parsedTemplate);
50-
setShowForm(false);
51-
onTemplateCreated();
52-
return;
53-
}
54-
5553
const response = await fetch(`/api/templates/${automationId}`, { signal });
5654
const data = await response.json();
5755

@@ -60,13 +58,17 @@ export const GenericTemplate = ({
6058
setTemplate(data.template);
6159
setShowForm(false);
6260
onTemplateCreated();
63-
} else if (!hasTemplates) {
61+
} else {
62+
// Clear cached template if it doesn't exist in database
63+
sessionStorage.removeItem(`template-${automationId}`);
6464
setShowForm(true);
65-
toast({
66-
title: "No Template Found",
67-
description: "Create Generic Template from the Drawer",
68-
variant: "destructive"
69-
});
65+
if (!hasTemplates) {
66+
toast({
67+
title: "No Template Found",
68+
description: "Create Generic Template from the Drawer",
69+
variant: "destructive"
70+
});
71+
}
7072
}
7173
} catch (error) {
7274
if (error instanceof Error && error.name !== 'AbortError') {
@@ -87,6 +89,7 @@ export const GenericTemplate = ({
8789

8890
const isValid = template.title.length > 0 &&
8991
template.imageUrl.length > 0 &&
92+
Array.isArray(template.buttons) &&
9093
template.buttons.length > 0 &&
9194
template.buttons.every(button =>
9295
button.title.length > 0 &&
@@ -124,52 +127,57 @@ export const GenericTemplate = ({
124127
}));
125128
};
126129

127-
const handleCreateTemplate = async () => {
128-
if (!isValid) {
129-
toast({
130-
title: "Missing Required Fields",
131-
description: "Please fill in title, image URL, and at least one valid button",
132-
variant: "destructive"
133-
});
134-
return;
135-
}
136-
137-
setIsSaving(true);
138-
try {
139-
const response = await fetch('/api/templates', {
140-
method: 'POST',
141-
headers: {
142-
'Content-Type': 'application/json',
143-
'Accept': 'application/json'
144-
},
145-
body: JSON.stringify({
146-
automationId,
147-
template: {
148-
...template,
149-
buttons: template.buttons.map(btn => ({
150-
...btn,
151-
title: btn.title.substring(0, 20)
152-
}))
153-
}
154-
})
155-
});
130+
const handleCreateTemplate = async () => {
131+
if (!isValid) {
132+
toast({
133+
title: "Missing Required Fields",
134+
description: "Please fill in title, image URL, and at least one valid button",
135+
variant: "destructive"
136+
});
137+
return;
138+
}
156139

157-
const data = await response.json();
158-
if (!response.ok) throw new Error(data.message || 'Failed to save template');
140+
setIsSaving(true);
141+
try {
142+
const response = await fetch('/api/templates', {
143+
method: 'POST',
144+
headers: {
145+
'Content-Type': 'application/json',
146+
'Accept': 'application/json'
147+
},
148+
body: JSON.stringify({
149+
automationId,
150+
template: {
151+
title: template.title,
152+
subtitle: template.subtitle,
153+
imageUrl: template.imageUrl,
154+
defaultAction: template.defaultAction,
155+
buttons: template.buttons.map(btn => ({
156+
...btn,
157+
title: btn.title.substring(0, 20)
158+
}))
159+
}
160+
})
161+
});
159162

160-
toast({ title: "Success", description: "Template saved successfully" });
161-
setShowForm(false);
162-
onTemplateCreated();
163-
} catch (error) {
164-
toast({
165-
title: "Error",
166-
description: error instanceof Error ? error.message : "Failed to save template",
167-
variant: "destructive"
168-
});
169-
} finally {
170-
setIsSaving(false);
171-
}
172-
};
163+
const data = await response.json();
164+
if (!response.ok) throw new Error(data.message || 'Failed to save template');
165+
166+
// Save to session storage and update UI
167+
sessionStorage.setItem(`template-${automationId}`, JSON.stringify(data));
168+
toast({ title: "Success", description: "Template saved successfully" });
169+
setShowForm(false);
170+
onTemplateCreated();
171+
} catch (error) {
172+
toast({
173+
title: "Error",
174+
description: error instanceof Error ? error.message : "Failed to save template",
175+
variant: "destructive"
176+
});
177+
} finally {
178+
setIsSaving(false);
179+
}
180+
};
173181

174182
if (!showForm) {
175183
return (
@@ -206,7 +214,7 @@ export const GenericTemplate = ({
206214
</p>
207215
)}
208216
<div className="space-y-2 pt-3">
209-
{template.buttons.map((button, index) => (
217+
{Array.isArray(template.buttons) && template.buttons.map((button, index) => (
210218
<button
211219
key={index}
212220
className="w-full p-3 text-center border-2 rounded-lg text-[#768ADD] border-[#768ADD] hover:bg-[#768ADD]/10 transition-all duration-200 font-medium text-sm"
@@ -290,12 +298,12 @@ export const GenericTemplate = ({
290298
<div>
291299
<label className="block text-sm font-medium text-gray-700 mb-2">Buttons (Max 3) *</label>
292300
<div className="space-y-3 max-h-[200px] min-h-[60px] overflow-y-auto scrollbar-thin scrollbar-track-gray-100">
293-
{template.buttons.map((button, btnIndex) => (
301+
{Array.isArray(template.buttons) && template.buttons.map((button, btnIndex) => (
294302
<div key={btnIndex} className="flex flex-col gap-2 w-full">
295303
<select
296304
className="w-full p-2 border border-gray-200 rounded-md !bg-white !text-black focus:outline-none"
297305
value={button.type}
298-
onChange={(e) => handleButtonChange(btnIndex, 'type', e.target.value)}
306+
onChange={(e) => handleButtonChange(btnIndex, 'type', e.target.value as 'web_url' | 'postback')}
299307
>
300308
<option value="web_url">Web URL</option>
301309
<option value="postback">Postback</option>
@@ -363,52 +371,7 @@ export const GenericTemplate = ({
363371

364372
<Dialog open={isPreviewOpen} onOpenChange={setIsPreviewOpen}>
365373
<DialogContent className="bg-white p-0 overflow-hidden max-w-sm rounded-lg shadow-lg">
366-
<div className="flex flex-col">
367-
{template.imageUrl ? (
368-
<div className="relative w-full h-48 overflow-hidden rounded-t-lg">
369-
<Image
370-
src={template.imageUrl}
371-
alt={template.title}
372-
fill
373-
className="object-cover transition-transform hover:scale-105"
374-
sizes="(max-width: 768px) 100vw, 400px"
375-
/>
376-
</div>
377-
) : (
378-
<div className="w-full h-48 bg-gray-100 rounded-t-lg flex items-center justify-center">
379-
<div className="text-gray-400 text-center p-4">
380-
<p className="text-sm">Preview your template</p>
381-
<p className="text-xs mt-1">Add an image URL to see it here</p>
382-
</div>
383-
</div>
384-
)}
385-
<div className="p-6 space-y-3">
386-
<h3 className="font-semibold text-xl text-gray-900">
387-
{template.title || 'Template Title'}
388-
</h3>
389-
{template.subtitle && (
390-
<p className="text-gray-600 text-sm leading-relaxed">
391-
{template.subtitle}
392-
</p>
393-
)}
394-
<div className="space-y-2 pt-3">
395-
{template.buttons.length > 0 ? (
396-
template.buttons.map((button, index) => (
397-
<button
398-
key={index}
399-
className="w-full p-3 text-center border-2 rounded-lg text-[#768ADD] border-[#768ADD] hover:bg-[#768ADD]/10 transition-all duration-200 font-medium text-sm"
400-
>
401-
{button.title || 'Button Text'}
402-
</button>
403-
))
404-
) : (
405-
<div className="text-center text-gray-400 text-sm">
406-
<p>Add buttons to see them here</p>
407-
</div>
408-
)}
409-
</div>
410-
</div>
411-
</div>
374+
<TemplatePreviewCard template={template} />
412375
</DialogContent>
413376
</Dialog>
414377
</div>

src/app/api/instagram-proxy/route.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//instagram proxy for 403 error on posts
2+
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
export async function GET(request: NextRequest) {
6+
try {
7+
const url = request.nextUrl.searchParams.get("url");
8+
if (!url) {
9+
return NextResponse.json({ error: "URL parameter is required" }, { status: 400 });
10+
}
11+
12+
const response = await fetch(url, {
13+
headers: {
14+
"User-Agent": "Mozilla/5.0",
15+
"Accept": "image/*"
16+
}
17+
});
18+
19+
if (!response.ok) {
20+
return NextResponse.json(
21+
{ error: `Failed to fetch image: ${response.statusText}` },
22+
{ status: response.status }
23+
);
24+
}
25+
26+
const contentType = response.headers.get("content-type");
27+
const buffer = await response.arrayBuffer();
28+
29+
return new NextResponse(buffer, {
30+
headers: {
31+
"Content-Type": contentType || "image/jpeg",
32+
"Cache-Control": "public, max-age=31536000",
33+
},
34+
});
35+
} catch (error) {
36+
console.error("Instagram proxy error:", error);
37+
return NextResponse.json(
38+
{ error: "Failed to proxy Instagram image" },
39+
{ status: 500 }
40+
);
41+
}
42+
}

0 commit comments

Comments
 (0)