SakibAhmed commited on
Commit
91d911a
·
verified ·
1 Parent(s): 36db83b

Upload 4 files

Browse files
Files changed (4) hide show
  1. Dockerfile +45 -0
  2. app.py +116 -0
  3. requirements.txt +3 -0
  4. templates/index.html +423 -0
Dockerfile ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use an official Python runtime as a parent image
2
+ FROM python:3.10-slim
3
+
4
+ # Set the working directory in the container
5
+ WORKDIR /app
6
+
7
+ # Install system dependencies
8
+ RUN apt-get update && apt-get install -y --no-install-recommends \
9
+ libgl1 \
10
+ libglib2.0-0 \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # Copy the requirements file
14
+ COPY requirements.txt requirements.txt
15
+
16
+ # Install Python packages with timeout increase
17
+ RUN pip install --no-cache-dir --timeout=1000 -r requirements.txt
18
+
19
+ # Copy application code
20
+ COPY . /app
21
+
22
+ # Create a non-root user
23
+ RUN useradd -m -u 1000 user
24
+
25
+ # Change ownership
26
+ RUN chown -R user:user /app
27
+
28
+ # Switch to the non-root user
29
+ USER user
30
+
31
+ # Expose the port
32
+ EXPOSE 7860
33
+
34
+ # Set environment variables
35
+ ENV FLASK_HOST=0.0.0.0
36
+ ENV FLASK_PORT=7860
37
+ ENV FLASK_DEBUG=False
38
+
39
+ # CRITICAL: Set HF-specific env vars
40
+ ENV TRANSFORMERS_CACHE=/tmp/transformers_cache
41
+ ENV HF_HOME=/tmp/hf_home
42
+ ENV TORCH_HOME=/tmp/torch_home
43
+
44
+ # Command to run the app
45
+ CMD ["python", "app.py"]
app.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, render_template, request, jsonify
2
+ import requests
3
+
4
+ app = Flask(__name__)
5
+
6
+ # Configuration - Your n8n webhook URL
7
+ N8N_WEBHOOK_URL = "https://n53dnorth.app.n8n.cloud/webhook/lead-intake"
8
+
9
+ @app.route('/')
10
+ def home():
11
+ return render_template('index.html')
12
+
13
+ @app.route('/submit', methods=['POST'])
14
+ def submit_form():
15
+ """
16
+ Receives form data and forwards it to n8n workflow
17
+ Maps form fields to the exact structure expected by n8n
18
+ """
19
+ try:
20
+ # 1. Get data from the web form
21
+ data = request.json
22
+
23
+ # 2. Prepare payload matching EXACTLY what n8n workflow expects
24
+ # Based on the sample: { first_name, last_name, email, city, source, budget }
25
+ payload = {
26
+ "first_name": data.get('first_name', '').strip(),
27
+ "last_name": data.get('last_name', '').strip(),
28
+ "email": data.get('email', '').strip(),
29
+ "city": data.get('city', '').strip(),
30
+ "source": data.get('source', 'Web Form').strip(),
31
+ "budget": int(data.get('budget', 5000)), # Convert to integer
32
+
33
+ # Optional fields (can be included for reference)
34
+ "company": data.get('company', '').strip() if data.get('company') else None,
35
+ "position": data.get('position', '').strip() if data.get('position') else None,
36
+ "phone": data.get('phone', '').strip() if data.get('phone') else None,
37
+ "address": data.get('address', '').strip() if data.get('address') else None,
38
+ "zip_code": data.get('zip_code', '').strip() if data.get('zip_code') else None,
39
+ }
40
+
41
+ # Remove None values to keep payload clean
42
+ payload = {k: v for k, v in payload.items() if v is not None}
43
+
44
+ print(f"Sending to n8n: {payload}") # Debug log
45
+
46
+ # 3. Send to n8n webhook
47
+ response = requests.post(
48
+ N8N_WEBHOOK_URL,
49
+ json=payload,
50
+ timeout=10 # 10 second timeout
51
+ )
52
+
53
+ print(f"n8n Response Status: {response.status_code}") # Debug log
54
+ print(f"n8n Response Body: {response.text}") # Debug log
55
+
56
+ # 4. Handle n8n's response
57
+ if response.status_code == 200:
58
+ # Success response from n8n
59
+ result = response.json()
60
+ return jsonify({
61
+ "status": "success",
62
+ "message": result.get("message", "Lead submitted successfully!"),
63
+ "data": result.get("data"),
64
+ "timestamp": result.get("timestamp")
65
+ }), 200
66
+
67
+ elif response.status_code == 400:
68
+ # Validation error from n8n
69
+ result = response.json()
70
+ return jsonify({
71
+ "status": "error",
72
+ "message": result.get("message", "Validation failed"),
73
+ "error": result.get("error")
74
+ }), 400
75
+
76
+ else:
77
+ # Unexpected response
78
+ return jsonify({
79
+ "status": "error",
80
+ "message": f"Unexpected response from server (Status: {response.status_code})"
81
+ }), 500
82
+
83
+ except requests.exceptions.Timeout:
84
+ return jsonify({
85
+ "status": "error",
86
+ "message": "Request timed out. Please try again."
87
+ }), 504
88
+
89
+ except requests.exceptions.ConnectionError:
90
+ return jsonify({
91
+ "status": "error",
92
+ "message": "Could not connect to the registration service. Please check your connection."
93
+ }), 503
94
+
95
+ except ValueError as e:
96
+ # JSON parsing error
97
+ return jsonify({
98
+ "status": "error",
99
+ "message": f"Invalid data format: {str(e)}"
100
+ }), 400
101
+
102
+ except Exception as e:
103
+ # Catch-all for any other errors
104
+ print(f"Unexpected error: {str(e)}") # Debug log
105
+ return jsonify({
106
+ "status": "error",
107
+ "message": "An unexpected error occurred. Please try again."
108
+ }), 500
109
+
110
+ @app.route('/health', methods=['GET'])
111
+ def health_check():
112
+ """Health check endpoint"""
113
+ return jsonify({"status": "healthy", "service": "Lead Registration API"}), 200
114
+
115
+ if __name__ == '__main__':
116
+ app.run(debug=True, port=5000)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ customtkinter==5.2.2
2
+ Flask==3.1.2
3
+ Requests==2.32.5
templates/index.html ADDED
@@ -0,0 +1,423 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Lead Registration</title>
7
+ <style>
8
+ /* CSS Reset & Base */
9
+ * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; }
10
+ body {
11
+ background: linear-gradient(135deg, #a1c4fd 0%, #c2e9fb 100%);
12
+ display: flex;
13
+ justify-content: center;
14
+ align-items: center;
15
+ min-height: 100vh;
16
+ padding: 20px;
17
+ }
18
+
19
+ /* Card Container */
20
+ .card {
21
+ display: flex;
22
+ background: white;
23
+ border-radius: 15px;
24
+ box-shadow: 0 10px 30px rgba(0,0,0,0.2);
25
+ overflow: hidden;
26
+ width: 1000px;
27
+ max-width: 100%;
28
+ min-height: 600px;
29
+ }
30
+
31
+ /* Left Panel (White) */
32
+ .left-panel {
33
+ flex: 1;
34
+ padding: 50px;
35
+ background: #fff;
36
+ }
37
+
38
+ /* Right Panel (Blue) */
39
+ .right-panel {
40
+ flex: 1;
41
+ padding: 50px;
42
+ background: #4d4dff;
43
+ color: white;
44
+ }
45
+
46
+ /* Headings */
47
+ h2 { margin-bottom: 30px; font-weight: 600; font-size: 1.5rem; }
48
+ .left-panel h2 { color: #3b4d81; }
49
+ .right-panel h2 { color: white; }
50
+
51
+ /* Form Grid System */
52
+ .form-row {
53
+ display: flex;
54
+ gap: 20px;
55
+ margin-bottom: 25px;
56
+ }
57
+ .form-group {
58
+ flex: 1;
59
+ position: relative;
60
+ }
61
+
62
+ /* Inputs - General Styling */
63
+ input, select {
64
+ width: 100%;
65
+ padding: 12px 0 8px 0;
66
+ border: none;
67
+ outline: none;
68
+ font-size: 0.95rem;
69
+ transition: 0.3s;
70
+ background: transparent;
71
+ }
72
+
73
+ /* Left Panel Input Styling (Light) */
74
+ .left-panel input, .left-panel select {
75
+ border-bottom: 2px solid #ddd;
76
+ color: #333;
77
+ }
78
+ .left-panel input::placeholder {
79
+ color: #999;
80
+ opacity: 1;
81
+ }
82
+ .left-panel input:focus, .left-panel select:focus {
83
+ border-bottom-color: #4d4dff;
84
+ }
85
+ .left-panel select {
86
+ cursor: pointer;
87
+ appearance: none;
88
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
89
+ background-repeat: no-repeat;
90
+ background-position: right center;
91
+ }
92
+ .left-panel select option {
93
+ color: #333;
94
+ background: white;
95
+ }
96
+
97
+ /* Right Panel Input Styling (Dark/Blue) */
98
+ .right-panel input, .right-panel select {
99
+ border-bottom: 2px solid rgba(255,255,255,0.4);
100
+ color: white;
101
+ }
102
+ .right-panel input::placeholder {
103
+ color: rgba(255,255,255,0.7);
104
+ opacity: 1;
105
+ }
106
+ .right-panel select {
107
+ cursor: pointer;
108
+ appearance: none;
109
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23ffffff' d='M6 9L1 4h10z'/%3E%3C/svg%3E");
110
+ background-repeat: no-repeat;
111
+ background-position: right center;
112
+ }
113
+ .right-panel select option {
114
+ color: #333;
115
+ background: #4d4dff;
116
+ }
117
+ .right-panel input:focus, .right-panel select:focus {
118
+ border-bottom-color: white;
119
+ }
120
+
121
+ /* Checkbox */
122
+ .checkbox-group {
123
+ display: flex;
124
+ align-items: center;
125
+ font-size: 0.85rem;
126
+ margin-top: 20px;
127
+ margin-bottom: 30px;
128
+ color: rgba(255,255,255,0.9);
129
+ }
130
+ .checkbox-group input {
131
+ width: auto;
132
+ margin-right: 10px;
133
+ cursor: pointer;
134
+ }
135
+ .checkbox-group label {
136
+ cursor: pointer;
137
+ }
138
+
139
+ /* Button */
140
+ .btn-submit {
141
+ background: white;
142
+ color: #4d4dff;
143
+ border: none;
144
+ padding: 12px 30px;
145
+ border-radius: 25px;
146
+ font-weight: bold;
147
+ cursor: pointer;
148
+ font-size: 1rem;
149
+ transition: all 0.2s;
150
+ }
151
+ .btn-submit:hover {
152
+ transform: scale(1.05);
153
+ box-shadow: 0 4px 15px rgba(255,255,255,0.3);
154
+ }
155
+ .btn-submit:disabled {
156
+ opacity: 0.6;
157
+ cursor: not-allowed;
158
+ transform: scale(1);
159
+ }
160
+
161
+ /* Alert Messages */
162
+ .alert {
163
+ padding: 15px 20px;
164
+ margin-bottom: 25px;
165
+ border-radius: 8px;
166
+ display: none;
167
+ font-size: 0.9rem;
168
+ animation: slideIn 0.3s ease-out;
169
+ }
170
+ .alert.success {
171
+ background: #d4edda;
172
+ color: #155724;
173
+ border-left: 4px solid #28a745;
174
+ }
175
+ .alert.error {
176
+ background: #f8d7da;
177
+ color: #721c24;
178
+ border-left: 4px solid #dc3545;
179
+ }
180
+ .alert.show { display: block; }
181
+
182
+ @keyframes slideIn {
183
+ from {
184
+ opacity: 0;
185
+ transform: translateY(-10px);
186
+ }
187
+ to {
188
+ opacity: 1;
189
+ transform: translateY(0);
190
+ }
191
+ }
192
+
193
+ /* Responsive Mobile */
194
+ @media (max-width: 768px) {
195
+ .card { flex-direction: column; }
196
+ .left-panel, .right-panel { padding: 30px; }
197
+ .form-row { flex-direction: column; gap: 15px; }
198
+ }
199
+ </style>
200
+ </head>
201
+ <body>
202
+
203
+ <div class="card">
204
+ <!-- Left Side: General Info -->
205
+ <div class="left-panel">
206
+ <h2>General Information</h2>
207
+
208
+ <div id="alertBox" class="alert"></div>
209
+
210
+ <form id="leadForm">
211
+ <div class="form-row">
212
+ <div class="form-group">
213
+ <input type="text" name="first_name" placeholder="First Name *" required>
214
+ </div>
215
+ <div class="form-group">
216
+ <input type="text" name="last_name" placeholder="Last Name *" required>
217
+ </div>
218
+ </div>
219
+
220
+ <div class="form-row">
221
+ <div class="form-group">
222
+ <input type="email" name="email" placeholder="Email Address *" required>
223
+ </div>
224
+ </div>
225
+
226
+ <div class="form-row">
227
+ <div class="form-group">
228
+ <select name="source" required>
229
+ <option value="" disabled selected>How did you hear about us? *</option>
230
+ <option value="Google Ads">Google Ads</option>
231
+ <option value="Facebook">Facebook</option>
232
+ <option value="LinkedIn">LinkedIn</option>
233
+ <option value="Twitter">Twitter</option>
234
+ <option value="Referral">Referral</option>
235
+ <option value="Direct">Direct</option>
236
+ <option value="Other">Other</option>
237
+ </select>
238
+ </div>
239
+ </div>
240
+
241
+ <div class="form-row">
242
+ <div class="form-group">
243
+ <input type="text" name="company" placeholder="Company Name (Optional)">
244
+ </div>
245
+ </div>
246
+
247
+ <div class="form-row">
248
+ <div class="form-group">
249
+ <select name="position">
250
+ <option value="" disabled selected>Position (Optional)</option>
251
+ <option value="Developer">Developer</option>
252
+ <option value="Manager">Manager</option>
253
+ <option value="Director">Director</option>
254
+ <option value="Executive">Executive</option>
255
+ <option value="Other">Other</option>
256
+ </select>
257
+ </div>
258
+ </div>
259
+ </div>
260
+
261
+ <!-- Right Side: Contact Details -->
262
+ <div class="right-panel">
263
+ <h2>Additional Information</h2>
264
+
265
+ <div class="form-row">
266
+ <div class="form-group">
267
+ <select name="city" required>
268
+ <option value="" disabled selected>City *</option>
269
+ <option value="Dhaka">Dhaka</option>
270
+ <option value="Chittagong">Chittagong</option>
271
+ <option value="Winnipeg">Winnipeg</option>
272
+ <option value="Toronto">Toronto</option>
273
+ <option value="Vancouver">Vancouver</option>
274
+ <option value="Montreal">Montreal</option>
275
+ <option value="Calgary">Calgary</option>
276
+ <option value="New York">New York</option>
277
+ <option value="Los Angeles">Los Angeles</option>
278
+ <option value="Chicago">Chicago</option>
279
+ <option value="Other">Other</option>
280
+ </select>
281
+ </div>
282
+ </div>
283
+
284
+ <div class="form-row">
285
+ <div class="form-group">
286
+ <select name="budget" required>
287
+ <option value="" disabled selected>Budget Range *</option>
288
+ <option value="1000">Under $1,000</option>
289
+ <option value="5000">$1,000 - $5,000</option>
290
+ <option value="10000">$5,000 - $10,000</option>
291
+ <option value="25000">$10,000 - $25,000</option>
292
+ <option value="50000">$25,000 - $50,000</option>
293
+ <option value="100000">$50,000+</option>
294
+ </select>
295
+ </div>
296
+ </div>
297
+
298
+ <div class="form-row">
299
+ <div class="form-group">
300
+ <input type="tel" name="phone" placeholder="Phone Number (Optional)">
301
+ </div>
302
+ </div>
303
+
304
+ <div class="form-row">
305
+ <div class="form-group">
306
+ <input type="text" name="address" placeholder="Street Address (Optional)">
307
+ </div>
308
+ </div>
309
+
310
+ <div class="form-row">
311
+ <div class="form-group">
312
+ <input type="text" name="zip_code" placeholder="Zip/Postal Code (Optional)">
313
+ </div>
314
+ </div>
315
+
316
+ <div class="checkbox-group">
317
+ <input type="checkbox" id="terms" required>
318
+ <label for="terms">I accept the Terms and Conditions *</label>
319
+ </div>
320
+
321
+ <button type="button" class="btn-submit" onclick="submitData()">Submit Lead</button>
322
+ </form>
323
+ </div>
324
+ </div>
325
+
326
+ <script>
327
+ async function submitData() {
328
+ const form = document.getElementById('leadForm');
329
+ const alertBox = document.getElementById('alertBox');
330
+
331
+ // Reset alert
332
+ alertBox.className = 'alert';
333
+ alertBox.style.display = 'none';
334
+
335
+ // Validate form
336
+ if (!form.checkValidity()) {
337
+ showAlert("Please fill in all required fields marked with *", "error");
338
+ form.reportValidity();
339
+ return;
340
+ }
341
+
342
+ // Gather form data
343
+ const formData = new FormData(form);
344
+ const data = Object.fromEntries(formData.entries());
345
+
346
+ // UI feedback
347
+ const btn = document.querySelector('.btn-submit');
348
+ const originalText = btn.innerText;
349
+ btn.innerText = "Submitting...";
350
+ btn.disabled = true;
351
+
352
+ try {
353
+ console.log("Sending data:", data); // Debug
354
+
355
+ const response = await fetch('/submit', {
356
+ method: 'POST',
357
+ headers: { 'Content-Type': 'application/json' },
358
+ body: JSON.stringify(data)
359
+ });
360
+
361
+ console.log("Response status:", response.status); // Debug
362
+
363
+ const result = await response.json();
364
+ console.log("Response data:", result); // Debug
365
+
366
+ if (response.ok) {
367
+ // Success!
368
+ const successMsg = result.message || "✅ Lead submitted successfully! We'll be in touch soon.";
369
+ showAlert(successMsg, "success");
370
+
371
+ // Reset form after success
372
+ setTimeout(() => {
373
+ form.reset();
374
+ }, 500);
375
+
376
+ // Log for debugging
377
+ if (result.data) {
378
+ console.log("Lead ID:", result.data.meta?.lead_id);
379
+ console.log("Full Response:", result);
380
+ }
381
+ } else {
382
+ // Error response
383
+ const errorMsg = result.error?.reason || result.message || "❌ Failed to submit lead";
384
+ showAlert(`Error: ${errorMsg}`, "error");
385
+ console.error("Error Response:", result);
386
+ }
387
+ } catch (error) {
388
+ // Network/connection error
389
+ showAlert(`❌ Connection error: ${error.message}. Please check your connection and try again.`, "error");
390
+ console.error("Network Error:", error);
391
+ } finally {
392
+ // Always reset button
393
+ btn.innerText = originalText;
394
+ btn.disabled = false;
395
+ }
396
+ }
397
+
398
+ function showAlert(message, type) {
399
+ const alertBox = document.getElementById('alertBox');
400
+ alertBox.textContent = message;
401
+ alertBox.className = `alert ${type} show`;
402
+
403
+ // Scroll to alert if not visible
404
+ alertBox.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
405
+
406
+ // Auto-hide success messages after 7 seconds
407
+ if (type === 'success') {
408
+ setTimeout(() => {
409
+ alertBox.className = 'alert';
410
+ }, 7000);
411
+ }
412
+ }
413
+
414
+ // Add enter key support
415
+ document.getElementById('leadForm').addEventListener('keypress', function(e) {
416
+ if (e.key === 'Enter' && e.target.tagName !== 'TEXTAREA') {
417
+ e.preventDefault();
418
+ submitData();
419
+ }
420
+ });
421
+ </script>
422
+ </body>
423
+ </html>