Skip to content

Commit de7d79f

Browse files
committed
Refactor server structure and enhance form generation features
- Removed the old server.ts file and integrated its functionality into server/index.ts. - Added express-rate-limit middleware to prevent abuse of the API. - Implemented request validation for form configuration in the export-to-google-forms endpoint. - Updated package.json to include new dependencies: express, cors, and express-rate-limit. - Enhanced the AIFormBuilder component with improved user instructions. - Added ErrorBoundary for better error handling in the app. - Updated form validation logic to enforce stricter rules and improve error messages. - Refined theme toggle functionality for better user experience. - Cleaned up unused components and styles.
1 parent 1a9e42d commit de7d79f

File tree

15 files changed

+293
-264
lines changed

15 files changed

+293
-264
lines changed

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,10 @@
2020
"@radix-ui/react-tabs": "^1.1.1",
2121
"class-variance-authority": "^0.7.0",
2222
"clsx": "^2.1.1",
23+
"cors": "^2.8.5",
2324
"dotenv": "^16.4.5",
25+
"express": "^4.21.1",
26+
"express-rate-limit": "^7.4.1",
2427
"googleapis": "^144.0.0",
2528
"lucide-react": "^0.454.0",
2629
"react": "^18.3.1",

server.ts

-45
This file was deleted.

server/index.ts

+87-65
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import express, { Request, Response } from "express";
22
import { google } from "googleapis";
33
import dotenv from "dotenv";
44
import cors from "cors";
5+
import rateLimit from "express-rate-limit";
6+
import { validateFormConfig } from "../src/utils/form-validation";
57

68
dotenv.config();
79

@@ -48,81 +50,101 @@ interface FormConfig {
4850
fields: FormField[];
4951
}
5052

51-
app.post("/api/export-to-google-forms", async (req: Request, res: Response) => {
52-
try {
53-
const formConfig: FormConfig = req.body;
53+
const limiter = rateLimit({
54+
windowMs: 15 * 60 * 1000, // 15 minutes
55+
max: 100, // limit each IP to 100 requests per windowMs
56+
});
5457

55-
if (!formConfig.title || !Array.isArray(formConfig.fields)) {
56-
return res.status(400).json({ error: "Invalid form configuration" });
57-
}
58+
app.use(limiter);
5859

59-
// First, create the form with just the title
60-
const form = await forms.forms.create({
61-
requestBody: {
62-
info: {
63-
title: formConfig.title,
60+
// Add request validation middleware
61+
const validateRequest = (req: Request, res: Response, next: Function) => {
62+
const errors = validateFormConfig(req.body);
63+
if (errors.length) {
64+
return res.status(400).json({ errors });
65+
}
66+
next();
67+
};
68+
69+
app.post(
70+
"/api/export-to-google-forms",
71+
validateRequest,
72+
async (req: Request, res: Response) => {
73+
try {
74+
const formConfig: FormConfig = req.body;
75+
76+
if (!formConfig.title || !Array.isArray(formConfig.fields)) {
77+
return res.status(400).json({ error: "Invalid form configuration" });
78+
}
79+
80+
// First, create the form with just the title
81+
const form = await forms.forms.create({
82+
requestBody: {
83+
info: {
84+
title: formConfig.title,
85+
},
6486
},
65-
},
66-
});
67-
68-
if (!form.data.formId) {
69-
throw new Error("No form ID received from Google Forms API");
70-
}
71-
72-
const formId = form.data.formId;
73-
74-
// Then, update the form with description and other info
75-
await forms.forms.batchUpdate({
76-
formId,
77-
requestBody: {
78-
requests: [
79-
// Add description if it exists
80-
...(formConfig.description
81-
? [
82-
{
83-
updateFormInfo: {
84-
info: {
85-
description: formConfig.description,
87+
});
88+
89+
if (!form.data.formId) {
90+
throw new Error("No form ID received from Google Forms API");
91+
}
92+
93+
const formId = form.data.formId;
94+
95+
// Then, update the form with description and other info
96+
await forms.forms.batchUpdate({
97+
formId,
98+
requestBody: {
99+
requests: [
100+
// Add description if it exists
101+
...(formConfig.description
102+
? [
103+
{
104+
updateFormInfo: {
105+
info: {
106+
description: formConfig.description,
107+
},
108+
updateMask: "description",
86109
},
87-
updateMask: "description",
88110
},
89-
},
90-
]
91-
: []),
92-
// Add all form fields
93-
...formConfig.fields.map((field, index) => ({
94-
createItem: {
95-
item: {
96-
title: field.label,
97-
description: field.helpText,
98-
questionItem: {
99-
question: {
100-
required: field.required,
101-
textQuestion: {
102-
paragraph: field.type === "textarea",
111+
]
112+
: []),
113+
// Add all form fields
114+
...formConfig.fields.map((field, index) => ({
115+
createItem: {
116+
item: {
117+
title: field.label,
118+
description: field.helpText,
119+
questionItem: {
120+
question: {
121+
required: field.required,
122+
textQuestion: {
123+
paragraph: field.type === "textarea",
124+
},
103125
},
104126
},
105127
},
128+
location: { index },
106129
},
107-
location: { index },
108-
},
109-
})),
110-
],
111-
},
112-
});
113-
114-
const formUrl = `https://docs.google.com/forms/d/${formId}/viewform`;
115-
console.log("Form created successfully:", formUrl);
116-
117-
res.json({ formUrl });
118-
} catch (error) {
119-
console.error("Google Forms API error:", error);
120-
res.status(500).json({
121-
error: "Failed to create Google Form",
122-
details: error instanceof Error ? error.message : "Unknown error",
123-
});
130+
})),
131+
],
132+
},
133+
});
134+
135+
const formUrl = `https://docs.google.com/forms/d/${formId}/viewform`;
136+
console.log("Form created successfully:", formUrl);
137+
138+
res.json({ formUrl });
139+
} catch (error) {
140+
console.error("Google Forms API error:", error);
141+
res.status(500).json({
142+
error: "Failed to create Google Form",
143+
details: error instanceof Error ? error.message : "Unknown error",
144+
});
145+
}
124146
}
125-
});
147+
);
126148

127149
// Health check endpoint
128150
app.get("/health", (req, res) => {

src/App.tsx

+11-7
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,20 @@ import { AIFormBuilder } from "./components/form-builder/AIFormBuilder";
33
import { FormPreview } from "./pages/FormPreview";
44
import { Toaster } from "sonner";
55
import { ThemeProvider } from "./components/providers/ThemeProvider";
6+
import { ErrorBoundary } from "./components/ErrorBoundary";
7+
68
function App() {
79
return (
810
<Router>
9-
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
10-
<Toaster position="top-center" richColors closeButton />
11-
<Routes>
12-
<Route path="/" element={<AIFormBuilder />} />
13-
<Route path="/preview" element={<FormPreview />} />
14-
</Routes>
15-
</ThemeProvider>
11+
<ErrorBoundary>
12+
<ThemeProvider defaultTheme="dark" storageKey="vite-ui-theme">
13+
<Toaster position="top-center" richColors closeButton />
14+
<Routes>
15+
<Route path="/" element={<AIFormBuilder />} />
16+
<Route path="/preview" element={<FormPreview />} />
17+
</Routes>
18+
</ThemeProvider>
19+
</ErrorBoundary>
1620
</Router>
1721
);
1822
}

src/components/ErrorBoundary.tsx

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from "react";
2+
import { Card } from "./ui/card";
3+
4+
interface Props {
5+
children: React.ReactNode;
6+
}
7+
8+
interface State {
9+
hasError: boolean;
10+
error?: Error;
11+
}
12+
13+
export class ErrorBoundary extends React.Component<Props, State> {
14+
constructor(props: Props) {
15+
super(props);
16+
this.state = { hasError: false };
17+
}
18+
19+
static getDerivedStateFromError(error: Error): State {
20+
return { hasError: true, error };
21+
}
22+
23+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
24+
console.error("Error caught by boundary:", error, errorInfo);
25+
}
26+
27+
render() {
28+
if (this.state.hasError) {
29+
return (
30+
<Card className="p-6 m-4 border-destructive">
31+
<h2 className="text-lg font-semibold text-destructive">
32+
Something went wrong
33+
</h2>
34+
<p className="mt-2 text-sm text-muted-foreground">
35+
Please try refreshing the page or contact support if the problem
36+
persists.
37+
</p>
38+
</Card>
39+
);
40+
}
41+
42+
return this.props.children;
43+
}
44+
}

src/components/form-builder/AIFormBuilder.tsx

+3-4
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,12 @@ export function AIFormBuilder() {
2525
</h1>
2626
</div>
2727
<p className="max-w-2xl mx-auto text-muted-foreground">
28-
Create professional forms instantly using AI. Describe your form
29-
requirements, and watch as AI generates a complete form structure
30-
for you.
28+
Just describe what you need, and watch AI create your perfect form
29+
in seconds. It's that simple - from idea to reality in a snap.
3130
</p>
3231
</div>
3332

34-
<Card className="overflow-hidden border-2">
33+
<Card className="overflow-hidden border-2 dark:bg-slate-800/55">
3534
<Tabs defaultValue="build" className="space-y-6">
3635
<div className="px-6 pt-6 pb-2 border-b bg-card">
3736
<TabsList>

src/components/form-builder/FormBuilderHeader.tsx

-1
This file was deleted.

src/components/form-builder/SettingsTab.tsx

+6-3
Original file line numberDiff line numberDiff line change
@@ -122,16 +122,19 @@ export function SettingsTab({ config, setConfig }: SettingsTabProps) {
122122

123123
<Button
124124
onClick={handleGoogleFormsExport}
125-
className="justify-start px-4 h-9 bg-muted/50 hover:bg-muted"
125+
className="relative justify-start px-4 h-9 text-muted-foreground hover:bg-muted group"
126126
variant="ghost"
127127
disabled={isExporting}
128128
>
129129
{isExporting ? (
130-
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
130+
<Loader2 className="w-4 h-4 mr-2 animate-spin " />
131131
) : (
132-
<FileSpreadsheet className="w-4 h-4 mr-2" />
132+
<FileSpreadsheet className="w-4 h-4 mr-2 " />
133133
)}
134134
Export to Google Forms
135+
<span className="absolute -top-1 -right-2 px-1.5 py-0.5 text-[10px] font-semibold tracking-wide rounded-full bg-primary/10 text-primary border border-primary/20">
136+
BETA
137+
</span>
135138
</Button>
136139

137140
<Button
+25-9
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,35 @@
11
import { Moon, Sun } from "lucide-react";
22
import { useTheme } from "../providers/ThemeProvider";
3-
import { Switch } from "../ui/switch";
3+
import { Button } from "../ui/button";
4+
import { cn } from "@/lib/utils";
45

56
export function ThemeToggle() {
67
const { theme, setTheme } = useTheme();
8+
const isDark = theme === "dark";
79

810
return (
9-
<div className="flex items-center space-x-2">
10-
<Sun className="w-4 h-4" />
11-
<Switch
12-
checked={theme === "dark"}
13-
onCheckedChange={(checked) => setTheme(checked ? "dark" : "light")}
14-
aria-label="Toggle theme"
11+
<Button
12+
variant="outline"
13+
size="icon"
14+
onClick={() => setTheme(isDark ? "light" : "dark")}
15+
className={cn(
16+
"relative h-9 w-9 rounded-md border border-input bg-background transition-colors hover:bg-accent",
17+
isDark && "hover:bg-accent/50"
18+
)}
19+
>
20+
<Sun
21+
className={cn(
22+
"h-4 w-4 rotate-0 scale-100 transition-all",
23+
isDark ? "rotate-90 scale-0" : "rotate-0 scale-100"
24+
)}
1525
/>
16-
<Moon className="w-4 h-4" />
17-
</div>
26+
<Moon
27+
className={cn(
28+
"absolute h-4 w-4 rotate-90 scale-0 transition-all",
29+
isDark ? "rotate-0 scale-100" : "rotate-90 scale-0"
30+
)}
31+
/>
32+
<span className="sr-only">Toggle theme</span>
33+
</Button>
1834
);
1935
}

0 commit comments

Comments
 (0)