Taf2023 commited on
Commit
f93593b
·
verified ·
1 Parent(s): a2dc0ea

Upload production-app.py

Browse files
Files changed (1) hide show
  1. production-app.py +978 -0
production-app.py ADDED
@@ -0,0 +1,978 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from datetime import datetime
3
+ from flask import Flask, request, jsonify, render_template_string
4
+ from flask_sqlalchemy import SQLAlchemy
5
+ from flask_cors import CORS
6
+ from werkzeug.serving import WSGIRequestHandler
7
+ import logging
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ app = Flask(__name__)
14
+ app.config['SECRET_KEY'] = 'asdf#FGSgvasgf$5$WGT'
15
+
16
+ # Enable CORS for all routes
17
+ CORS(app)
18
+
19
+ # Database configuration
20
+ app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///app.db'
21
+ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
22
+ db = SQLAlchemy(app)
23
+
24
+ # Database Models
25
+ class Post(db.Model):
26
+ id = db.Column(db.Integer, primary_key=True)
27
+ name = db.Column(db.String(100), nullable=False)
28
+ note = db.Column(db.Text, nullable=False)
29
+ youtube_link = db.Column(db.String(500), nullable=True)
30
+ likes = db.Column(db.Integer, default=0)
31
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
32
+
33
+ def to_dict(self):
34
+ return {
35
+ 'id': self.id,
36
+ 'name': self.name,
37
+ 'note': self.note,
38
+ 'youtube_link': self.youtube_link,
39
+ 'likes': self.likes,
40
+ 'created_at': self.created_at.isoformat() if self.created_at else None
41
+ }
42
+
43
+ class Comment(db.Model):
44
+ id = db.Column(db.Integer, primary_key=True)
45
+ post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False)
46
+ name = db.Column(db.String(100), nullable=False)
47
+ comment = db.Column(db.Text, nullable=False)
48
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
49
+
50
+ def to_dict(self):
51
+ return {
52
+ 'id': self.id,
53
+ 'post_id': self.post_id,
54
+ 'name': self.name,
55
+ 'comment': self.comment,
56
+ 'created_at': self.created_at.isoformat() if self.created_at else None
57
+ }
58
+
59
+ # Create database tables
60
+ with app.app_context():
61
+ db.create_all()
62
+
63
+ # HTML Template (English Version)
64
+ HTML_TEMPLATE = '''
65
+ <!DOCTYPE html>
66
+ <html lang="en">
67
+ <head>
68
+ <meta charset="UTF-8">
69
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
70
+ <title>Share YouTube - Share Videos and Notes with Friends</title>
71
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
72
+ <style>
73
+ * {
74
+ margin: 0;
75
+ padding: 0;
76
+ box-sizing: border-box;
77
+ }
78
+
79
+ body {
80
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
81
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
82
+ min-height: 100vh;
83
+ color: #333;
84
+ }
85
+
86
+ .container {
87
+ max-width: 800px;
88
+ margin: 0 auto;
89
+ padding: 20px;
90
+ }
91
+
92
+ .header {
93
+ text-align: center;
94
+ margin-bottom: 30px;
95
+ background: rgba(255, 255, 255, 0.95);
96
+ padding: 30px;
97
+ border-radius: 15px;
98
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
99
+ backdrop-filter: blur(10px);
100
+ }
101
+
102
+ .header h1 {
103
+ font-size: 2.5rem;
104
+ color: #FF0000;
105
+ margin-bottom: 10px;
106
+ text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.1);
107
+ }
108
+
109
+ .header p {
110
+ font-size: 1.1rem;
111
+ color: #666;
112
+ }
113
+
114
+ .share-section {
115
+ margin-bottom: 30px;
116
+ }
117
+
118
+ .share-card {
119
+ background: rgba(255, 255, 255, 0.95);
120
+ padding: 30px;
121
+ border-radius: 15px;
122
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
123
+ backdrop-filter: blur(10px);
124
+ }
125
+
126
+ .share-card h2 {
127
+ color: #333;
128
+ margin-bottom: 25px;
129
+ font-size: 1.5rem;
130
+ text-align: center;
131
+ }
132
+
133
+ .form-group {
134
+ margin-bottom: 20px;
135
+ }
136
+
137
+ .form-group label {
138
+ display: block;
139
+ margin-bottom: 8px;
140
+ font-weight: 600;
141
+ color: #555;
142
+ }
143
+
144
+ .form-group input,
145
+ .form-group textarea {
146
+ width: 100%;
147
+ padding: 12px 15px;
148
+ border: 2px solid #e1e5e9;
149
+ border-radius: 10px;
150
+ font-size: 1rem;
151
+ transition: all 0.3s ease;
152
+ background: rgba(255, 255, 255, 0.9);
153
+ }
154
+
155
+ .form-group input:focus,
156
+ .form-group textarea:focus {
157
+ outline: none;
158
+ border-color: #FF0000;
159
+ box-shadow: 0 0 0 3px rgba(255, 0, 0, 0.1);
160
+ transform: translateY(-2px);
161
+ }
162
+
163
+ .form-group textarea {
164
+ resize: vertical;
165
+ min-height: 80px;
166
+ }
167
+
168
+ .btn-share {
169
+ width: 100%;
170
+ padding: 15px;
171
+ background: linear-gradient(45deg, #FF0000, #CC0000);
172
+ color: white;
173
+ border: none;
174
+ border-radius: 10px;
175
+ font-size: 1.1rem;
176
+ font-weight: 600;
177
+ cursor: pointer;
178
+ transition: all 0.3s ease;
179
+ text-transform: uppercase;
180
+ letter-spacing: 1px;
181
+ }
182
+
183
+ .btn-share:hover {
184
+ transform: translateY(-2px);
185
+ box-shadow: 0 8px 25px rgba(255, 0, 0, 0.3);
186
+ }
187
+
188
+ .feed-section {
189
+ background: rgba(255, 255, 255, 0.95);
190
+ padding: 30px;
191
+ border-radius: 15px;
192
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
193
+ backdrop-filter: blur(10px);
194
+ }
195
+
196
+ .feed-section h2 {
197
+ color: #333;
198
+ margin-bottom: 25px;
199
+ font-size: 1.5rem;
200
+ text-align: center;
201
+ }
202
+
203
+ .link-card {
204
+ background: #fff;
205
+ border: 1px solid #e1e5e9;
206
+ border-radius: 12px;
207
+ padding: 20px;
208
+ margin-bottom: 20px;
209
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.05);
210
+ transition: all 0.3s ease;
211
+ }
212
+
213
+ .link-card:hover {
214
+ transform: translateY(-3px);
215
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
216
+ }
217
+
218
+ .link-header {
219
+ display: flex;
220
+ align-items: center;
221
+ margin-bottom: 15px;
222
+ }
223
+
224
+ .link-header .user-name {
225
+ font-weight: 600;
226
+ color: #333;
227
+ margin-right: 10px;
228
+ }
229
+
230
+ .link-header .timestamp {
231
+ color: #888;
232
+ font-size: 0.9rem;
233
+ }
234
+
235
+ .link-note {
236
+ margin-bottom: 15px;
237
+ color: #555;
238
+ line-height: 1.5;
239
+ }
240
+
241
+ .youtube-embed {
242
+ margin-bottom: 15px;
243
+ border-radius: 8px;
244
+ overflow: hidden;
245
+ }
246
+
247
+ .youtube-embed iframe {
248
+ width: 100%;
249
+ height: 315px;
250
+ border: none;
251
+ }
252
+
253
+ .link-actions {
254
+ display: flex;
255
+ gap: 15px;
256
+ padding-top: 15px;
257
+ border-top: 1px solid #e1e5e9;
258
+ }
259
+
260
+ .action-btn {
261
+ background: none;
262
+ border: none;
263
+ padding: 8px 15px;
264
+ border-radius: 20px;
265
+ cursor: pointer;
266
+ transition: all 0.3s ease;
267
+ font-size: 0.9rem;
268
+ display: flex;
269
+ align-items: center;
270
+ gap: 5px;
271
+ }
272
+
273
+ .like-btn {
274
+ color: #666;
275
+ }
276
+
277
+ .like-btn:hover,
278
+ .like-btn.liked {
279
+ background: rgba(255, 0, 0, 0.1);
280
+ color: #FF0000;
281
+ }
282
+
283
+ .comment-btn {
284
+ color: #666;
285
+ }
286
+
287
+ .comment-btn:hover {
288
+ background: rgba(0, 123, 255, 0.1);
289
+ color: #007bff;
290
+ }
291
+
292
+ .comments-section {
293
+ margin-top: 15px;
294
+ padding-top: 15px;
295
+ border-top: 1px solid #e1e5e9;
296
+ }
297
+
298
+ .comment-form {
299
+ display: flex;
300
+ gap: 10px;
301
+ margin-bottom: 15px;
302
+ }
303
+
304
+ .comment-form input {
305
+ flex: 1;
306
+ padding: 8px 12px;
307
+ border: 1px solid #ddd;
308
+ border-radius: 20px;
309
+ font-size: 0.9rem;
310
+ }
311
+
312
+ .comment-form button {
313
+ padding: 8px 15px;
314
+ background: #007bff;
315
+ color: white;
316
+ border: none;
317
+ border-radius: 20px;
318
+ cursor: pointer;
319
+ font-size: 0.9rem;
320
+ }
321
+
322
+ .comment-form button:hover {
323
+ background: #0056b3;
324
+ }
325
+
326
+ .comment {
327
+ background: #f8f9fa;
328
+ padding: 10px 15px;
329
+ border-radius: 10px;
330
+ margin-bottom: 10px;
331
+ }
332
+
333
+ .comment-author {
334
+ font-weight: 600;
335
+ color: #333;
336
+ margin-bottom: 5px;
337
+ }
338
+
339
+ .comment-text {
340
+ color: #555;
341
+ font-size: 0.9rem;
342
+ }
343
+
344
+ .loading-spinner {
345
+ position: fixed;
346
+ top: 50%;
347
+ left: 50%;
348
+ transform: translate(-50%, -50%);
349
+ background: rgba(255, 255, 255, 0.95);
350
+ padding: 30px;
351
+ border-radius: 15px;
352
+ text-align: center;
353
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.2);
354
+ display: none;
355
+ }
356
+
357
+ .loading-spinner i {
358
+ font-size: 2rem;
359
+ color: #FF0000;
360
+ margin-bottom: 10px;
361
+ }
362
+
363
+ .success-message {
364
+ position: fixed;
365
+ top: 20px;
366
+ right: 20px;
367
+ background: #28a745;
368
+ color: white;
369
+ padding: 15px 20px;
370
+ border-radius: 10px;
371
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.2);
372
+ display: none;
373
+ align-items: center;
374
+ gap: 10px;
375
+ z-index: 1000;
376
+ }
377
+
378
+ .success-message i {
379
+ font-size: 1.2rem;
380
+ }
381
+
382
+ @media (max-width: 768px) {
383
+ .container {
384
+ padding: 15px;
385
+ }
386
+
387
+ .header h1 {
388
+ font-size: 2rem;
389
+ }
390
+
391
+ .share-card,
392
+ .feed-section {
393
+ padding: 20px;
394
+ }
395
+
396
+ .youtube-embed iframe {
397
+ height: 200px;
398
+ }
399
+
400
+ .link-actions {
401
+ flex-wrap: wrap;
402
+ }
403
+ }
404
+
405
+ @keyframes fadeInUp {
406
+ from {
407
+ opacity: 0;
408
+ transform: translateY(30px);
409
+ }
410
+ to {
411
+ opacity: 1;
412
+ transform: translateY(0);
413
+ }
414
+ }
415
+
416
+ .link-card {
417
+ animation: fadeInUp 0.5s ease;
418
+ }
419
+ </style>
420
+ </head>
421
+ <body>
422
+ <div class="container">
423
+ <!-- Header -->
424
+ <header class="header">
425
+ <div class="header-content">
426
+ <h1><i class="fab fa-youtube"></i> Share YouTube</h1>
427
+ <p>Share videos and notes with friends</p>
428
+ </div>
429
+ </header>
430
+
431
+ <!-- Share Form -->
432
+ <div class="share-section">
433
+ <div class="share-card">
434
+ <h2><i class="fas fa-share"></i> Share YouTube Link</h2>
435
+ <form id="shareForm">
436
+ <div class="form-group">
437
+ <label for="name"><i class="fas fa-user"></i> Your Name:</label>
438
+ <input type="text" id="name" name="name" required placeholder="Enter your name">
439
+ </div>
440
+
441
+ <div class="form-group">
442
+ <label for="youtube_link"><i class="fab fa-youtube"></i> YouTube Link:</label>
443
+ <input type="url" id="youtube_link" name="youtube_link" required placeholder="https://www.youtube.com/watch?v=...">
444
+ </div>
445
+
446
+ <div class="form-group">
447
+ <label for="note"><i class="fas fa-sticky-note"></i> Short Note:</label>
448
+ <textarea id="note" name="note" required placeholder="Write a short note about this video..."></textarea>
449
+ </div>
450
+
451
+ <button type="submit" class="btn-share">
452
+ <i class="fas fa-share"></i> Share
453
+ </button>
454
+ </form>
455
+ </div>
456
+ </div>
457
+
458
+ <!-- Shared Links Feed -->
459
+ <div class="feed-section">
460
+ <h2><i class="fas fa-list"></i> Shared Links</h2>
461
+ <div id="linksContainer">
462
+ <!-- Shared links will be loaded here -->
463
+ </div>
464
+ </div>
465
+ </div>
466
+
467
+ <!-- Loading Spinner -->
468
+ <div id="loadingSpinner" class="loading-spinner">
469
+ <i class="fas fa-spinner fa-spin"></i>
470
+ <p>Loading...</p>
471
+ </div>
472
+
473
+ <!-- Success Message -->
474
+ <div id="successMessage" class="success-message">
475
+ <i class="fas fa-check-circle"></i>
476
+ <span id="successText">Shared successfully!</span>
477
+ </div>
478
+
479
+ <script>
480
+ // API Base URL
481
+ const API_BASE_URL = '/api';
482
+
483
+ // DOM Elements
484
+ const shareForm = document.getElementById('shareForm');
485
+ const linksContainer = document.getElementById('linksContainer');
486
+ const loadingSpinner = document.getElementById('loadingSpinner');
487
+ const successMessage = document.getElementById('successMessage');
488
+ const successText = document.getElementById('successText');
489
+
490
+ // Initialize app
491
+ document.addEventListener('DOMContentLoaded', function() {
492
+ loadLinks();
493
+
494
+ // Handle form submission
495
+ shareForm.addEventListener('submit', handleShareSubmit);
496
+ });
497
+
498
+ // Handle share form submission
499
+ async function handleShareSubmit(e) {
500
+ e.preventDefault();
501
+
502
+ const formData = new FormData(shareForm);
503
+ const data = {
504
+ name: formData.get('name'),
505
+ note: formData.get('note'),
506
+ youtube_link: formData.get('youtube_link')
507
+ };
508
+
509
+ // Validate YouTube URL
510
+ if (!isValidYouTubeURL(data.youtube_link)) {
511
+ alert('Please enter a valid YouTube URL');
512
+ return;
513
+ }
514
+
515
+ try {
516
+ showLoading(true);
517
+
518
+ const response = await fetch(`${API_BASE_URL}/posts`, {
519
+ method: 'POST',
520
+ headers: {
521
+ 'Content-Type': 'application/json',
522
+ },
523
+ body: JSON.stringify(data)
524
+ });
525
+
526
+ if (!response.ok) {
527
+ throw new Error('Failed to share link');
528
+ }
529
+
530
+ const result = await response.json();
531
+
532
+ // Show success message
533
+ showSuccessMessage('Shared successfully!');
534
+
535
+ // Clear form
536
+ shareForm.reset();
537
+
538
+ // Reload links
539
+ await loadLinks();
540
+
541
+ } catch (error) {
542
+ console.error('Error sharing link:', error);
543
+ alert('Error sharing link: ' + error.message);
544
+ } finally {
545
+ showLoading(false);
546
+ }
547
+ }
548
+
549
+ // Load all shared links
550
+ async function loadLinks() {
551
+ try {
552
+ showLoading(true);
553
+
554
+ const response = await fetch(`${API_BASE_URL}/posts`);
555
+ if (!response.ok) {
556
+ throw new Error('Failed to load links');
557
+ }
558
+
559
+ const links = await response.json();
560
+ displayLinks(links);
561
+
562
+ } catch (error) {
563
+ console.error('Error loading links:', error);
564
+ linksContainer.innerHTML = `
565
+ <div style="text-align: center; color: #666; padding: 20px;">
566
+ <i class="fas fa-exclamation-triangle"></i>
567
+ <p>Failed to load links: ${error.message}</p>
568
+ </div>
569
+ `;
570
+ } finally {
571
+ showLoading(false);
572
+ }
573
+ }
574
+
575
+ // Display links in the container
576
+ function displayLinks(links) {
577
+ if (links.length === 0) {
578
+ linksContainer.innerHTML = `
579
+ <div style="text-align: center; color: #666; padding: 40px;">
580
+ <i class="fab fa-youtube" style="font-size: 3rem; margin-bottom: 15px;"></i>
581
+ <p>No shared links yet</p>
582
+ <p>Be the first to share a YouTube link!</p>
583
+ </div>
584
+ `;
585
+ return;
586
+ }
587
+
588
+ linksContainer.innerHTML = links.map(link => createLinkCard(link)).join('');
589
+
590
+ // Add event listeners for like and comment buttons
591
+ addLinkEventListeners();
592
+ }
593
+
594
+ // Create HTML for a single link card
595
+ function createLinkCard(link) {
596
+ const videoId = extractYouTubeVideoId(link.youtube_link);
597
+ const embedUrl = videoId ? `https://www.youtube.com/embed/${videoId}` : '';
598
+ const timeAgo = getTimeAgo(new Date(link.created_at));
599
+
600
+ return `
601
+ <div class="link-card" data-link-id="${link.id}">
602
+ <div class="link-header">
603
+ <span class="user-name">
604
+ <i class="fas fa-user"></i> ${escapeHtml(link.name)}
605
+ </span>
606
+ <span class="timestamp">${timeAgo}</span>
607
+ </div>
608
+
609
+ <div class="link-note">
610
+ ${escapeHtml(link.note)}
611
+ </div>
612
+
613
+ ${embedUrl ? `
614
+ <div class="youtube-embed">
615
+ <iframe src="${embedUrl}"
616
+ title="YouTube video player"
617
+ frameborder="0"
618
+ allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
619
+ allowfullscreen>
620
+ </iframe>
621
+ </div>
622
+ ` : `
623
+ <div class="youtube-link">
624
+ <a href="${link.youtube_link}" target="_blank" rel="noopener noreferrer">
625
+ <i class="fab fa-youtube"></i> Watch on YouTube
626
+ </a>
627
+ </div>
628
+ `}
629
+
630
+ <div class="link-actions">
631
+ <button class="action-btn like-btn" data-link-id="${link.id}">
632
+ <i class="fas fa-heart"></i>
633
+ <span class="like-count">${link.likes}</span>
634
+ </button>
635
+ <button class="action-btn comment-btn" data-link-id="${link.id}">
636
+ <i class="fas fa-comment"></i>
637
+ Comment
638
+ </button>
639
+ </div>
640
+
641
+ <div class="comments-section" id="comments-${link.id}" style="display: none;">
642
+ <div class="comment-form">
643
+ <input type="text" placeholder="Write a comment..." class="comment-input">
644
+ <button type="button" class="add-comment-btn" data-link-id="${link.id}">
645
+ <i class="fas fa-paper-plane"></i>
646
+ </button>
647
+ </div>
648
+ <div class="comments-list" id="comments-list-${link.id}">
649
+ <!-- Comments will be loaded here -->
650
+ </div>
651
+ </div>
652
+ </div>
653
+ `;
654
+ }
655
+
656
+ // Add event listeners for link interactions
657
+ function addLinkEventListeners() {
658
+ // Like buttons
659
+ document.querySelectorAll('.like-btn').forEach(btn => {
660
+ btn.addEventListener('click', handleLike);
661
+ });
662
+
663
+ // Comment buttons
664
+ document.querySelectorAll('.comment-btn').forEach(btn => {
665
+ btn.addEventListener('click', toggleComments);
666
+ });
667
+
668
+ // Add comment buttons
669
+ document.querySelectorAll('.add-comment-btn').forEach(btn => {
670
+ btn.addEventListener('click', handleAddComment);
671
+ });
672
+
673
+ // Enter key for comment input
674
+ document.querySelectorAll('.comment-input').forEach(input => {
675
+ input.addEventListener('keypress', function(e) {
676
+ if (e.key === 'Enter') {
677
+ const linkId = this.closest('.comments-section').id.split('-')[1];
678
+ const btn = document.querySelector(`.add-comment-btn[data-link-id="${linkId}"]`);
679
+ btn.click();
680
+ }
681
+ });
682
+ });
683
+ }
684
+
685
+ // Handle like button click
686
+ async function handleLike(e) {
687
+ const linkId = e.currentTarget.dataset.linkId;
688
+ const likeBtn = e.currentTarget;
689
+ const likeCount = likeBtn.querySelector('.like-count');
690
+
691
+ try {
692
+ const response = await fetch(`${API_BASE_URL}/posts/${linkId}/like`, {
693
+ method: 'POST'
694
+ });
695
+
696
+ if (!response.ok) {
697
+ throw new Error('Failed to like post');
698
+ }
699
+
700
+ const result = await response.json();
701
+ likeCount.textContent = result.likes;
702
+
703
+ // Add visual feedback
704
+ likeBtn.classList.add('liked');
705
+ setTimeout(() => likeBtn.classList.remove('liked'), 1000);
706
+
707
+ } catch (error) {
708
+ console.error('Error liking post:', error);
709
+ alert('Error liking post');
710
+ }
711
+ }
712
+
713
+ // Toggle comments section
714
+ async function toggleComments(e) {
715
+ const linkId = e.currentTarget.dataset.linkId;
716
+ const commentsSection = document.getElementById(`comments-${linkId}`);
717
+
718
+ if (commentsSection.style.display === 'none') {
719
+ commentsSection.style.display = 'block';
720
+ await loadComments(linkId);
721
+ } else {
722
+ commentsSection.style.display = 'none';
723
+ }
724
+ }
725
+
726
+ // Load comments for a specific link
727
+ async function loadComments(linkId) {
728
+ try {
729
+ const response = await fetch(`${API_BASE_URL}/posts/${linkId}/comments`);
730
+ if (!response.ok) {
731
+ throw new Error('Failed to load comments');
732
+ }
733
+
734
+ const comments = await response.json();
735
+ const commentsList = document.getElementById(`comments-list-${linkId}`);
736
+
737
+ if (comments.length === 0) {
738
+ commentsList.innerHTML = '<p style="text-align: center; color: #666; padding: 10px;">No comments yet</p>';
739
+ } else {
740
+ commentsList.innerHTML = comments.map(comment => `
741
+ <div class="comment">
742
+ <div class="comment-author">
743
+ <i class="fas fa-user"></i> ${escapeHtml(comment.name)}
744
+ </div>
745
+ <div class="comment-text">${escapeHtml(comment.comment)}</div>
746
+ </div>
747
+ `).join('');
748
+ }
749
+
750
+ } catch (error) {
751
+ console.error('Error loading comments:', error);
752
+ }
753
+ }
754
+
755
+ // Handle add comment
756
+ async function handleAddComment(e) {
757
+ const linkId = e.currentTarget.dataset.linkId;
758
+ const commentsSection = document.getElementById(`comments-${linkId}`);
759
+ const commentInput = commentsSection.querySelector('.comment-input');
760
+ const commentText = commentInput.value.trim();
761
+
762
+ if (!commentText) {
763
+ alert('Please write a comment');
764
+ return;
765
+ }
766
+
767
+ // Simple name prompt (in real app, you'd have user authentication)
768
+ const name = prompt('Please enter your name:');
769
+ if (!name) return;
770
+
771
+ try {
772
+ const response = await fetch(`${API_BASE_URL}/posts/${linkId}/comments`, {
773
+ method: 'POST',
774
+ headers: {
775
+ 'Content-Type': 'application/json',
776
+ },
777
+ body: JSON.stringify({
778
+ name: name,
779
+ comment: commentText
780
+ })
781
+ });
782
+
783
+ if (!response.ok) {
784
+ throw new Error('Failed to add comment');
785
+ }
786
+
787
+ // Clear input
788
+ commentInput.value = '';
789
+
790
+ // Reload comments
791
+ await loadComments(linkId);
792
+
793
+ showSuccessMessage('Comment added successfully!');
794
+
795
+ } catch (error) {
796
+ console.error('Error adding comment:', error);
797
+ alert('Error adding comment');
798
+ }
799
+ }
800
+
801
+ // Utility functions
802
+ function isValidYouTubeURL(url) {
803
+ const youtubeRegex = /^(https?:\/\/)?(www\.)?(youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)[a-zA-Z0-9_-]{11}/;
804
+ return youtubeRegex.test(url);
805
+ }
806
+
807
+ function extractYouTubeVideoId(url) {
808
+ const regex = /(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/;
809
+ const match = url.match(regex);
810
+ return match ? match[1] : null;
811
+ }
812
+
813
+ function escapeHtml(text) {
814
+ const div = document.createElement('div');
815
+ div.textContent = text;
816
+ return div.innerHTML;
817
+ }
818
+
819
+ function getTimeAgo(date) {
820
+ const now = new Date();
821
+ const diffInSeconds = Math.floor((now - date) / 1000);
822
+
823
+ if (diffInSeconds < 60) return 'just now';
824
+ if (diffInSeconds < 3600) return `${Math.floor(diffInSeconds / 60)} minutes ago`;
825
+ if (diffInSeconds < 86400) return `${Math.floor(diffInSeconds / 3600)} hours ago`;
826
+ return `${Math.floor(diffInSeconds / 86400)} days ago`;
827
+ }
828
+
829
+ function showLoading(show) {
830
+ loadingSpinner.style.display = show ? 'block' : 'none';
831
+ }
832
+
833
+ function showSuccessMessage(message) {
834
+ successText.textContent = message;
835
+ successMessage.style.display = 'flex';
836
+ setTimeout(() => {
837
+ successMessage.style.display = 'none';
838
+ }, 3000);
839
+ }
840
+ </script>
841
+ </body>
842
+ </html>
843
+ '''
844
+
845
+ # API Routes
846
+ @app.route('/api/posts', methods=['GET'])
847
+ def get_posts():
848
+ """Get all posts"""
849
+ try:
850
+ posts = Post.query.order_by(Post.created_at.desc()).all()
851
+ return jsonify([post.to_dict() for post in posts])
852
+ except Exception as e:
853
+ logger.error(f"Error getting posts: {str(e)}")
854
+ return jsonify({'error': 'Failed to get posts'}), 500
855
+
856
+ @app.route('/api/posts', methods=['POST'])
857
+ def create_post():
858
+ """Create a new post"""
859
+ try:
860
+ data = request.get_json()
861
+
862
+ if not data or not data.get('name') or not data.get('note'):
863
+ return jsonify({'error': 'Name and note are required'}), 400
864
+
865
+ post = Post(
866
+ name=data['name'],
867
+ note=data['note'],
868
+ youtube_link=data.get('youtube_link', '')
869
+ )
870
+
871
+ db.session.add(post)
872
+ db.session.commit()
873
+
874
+ logger.info(f"New post created: {post.id}")
875
+ return jsonify(post.to_dict()), 201
876
+
877
+ except Exception as e:
878
+ logger.error(f"Error creating post: {str(e)}")
879
+ db.session.rollback()
880
+ return jsonify({'error': 'Failed to create post'}), 500
881
+
882
+ @app.route('/api/posts/<int:post_id>/like', methods=['POST'])
883
+ def like_post(post_id):
884
+ """Like a post"""
885
+ try:
886
+ post = Post.query.get_or_404(post_id)
887
+ post.likes += 1
888
+ db.session.commit()
889
+
890
+ logger.info(f"Post {post_id} liked, total likes: {post.likes}")
891
+ return jsonify({'likes': post.likes})
892
+
893
+ except Exception as e:
894
+ logger.error(f"Error liking post {post_id}: {str(e)}")
895
+ db.session.rollback()
896
+ return jsonify({'error': 'Failed to like post'}), 500
897
+
898
+ @app.route('/api/posts/<int:post_id>/comments', methods=['GET'])
899
+ def get_comments(post_id):
900
+ """Get comments for a post"""
901
+ try:
902
+ comments = Comment.query.filter_by(post_id=post_id).order_by(Comment.created_at.asc()).all()
903
+ return jsonify([comment.to_dict() for comment in comments])
904
+
905
+ except Exception as e:
906
+ logger.error(f"Error getting comments for post {post_id}: {str(e)}")
907
+ return jsonify({'error': 'Failed to get comments'}), 500
908
+
909
+ @app.route('/api/posts/<int:post_id>/comments', methods=['POST'])
910
+ def add_comment(post_id):
911
+ """Add a comment to a post"""
912
+ try:
913
+ data = request.get_json()
914
+
915
+ if not data or not data.get('name') or not data.get('comment'):
916
+ return jsonify({'error': 'Name and comment are required'}), 400
917
+
918
+ # Check if post exists
919
+ post = Post.query.get_or_404(post_id)
920
+
921
+ comment = Comment(
922
+ post_id=post_id,
923
+ name=data['name'],
924
+ comment=data['comment']
925
+ )
926
+
927
+ db.session.add(comment)
928
+ db.session.commit()
929
+
930
+ logger.info(f"New comment added to post {post_id}")
931
+ return jsonify(comment.to_dict()), 201
932
+
933
+ except Exception as e:
934
+ logger.error(f"Error adding comment to post {post_id}: {str(e)}")
935
+ db.session.rollback()
936
+ return jsonify({'error': 'Failed to add comment'}), 500
937
+
938
+ # Health check endpoint
939
+ @app.route('/health')
940
+ def health_check():
941
+ """Health check endpoint"""
942
+ return jsonify({'status': 'healthy', 'timestamp': datetime.utcnow().isoformat()})
943
+
944
+ # Main route
945
+ @app.route('/')
946
+ def index():
947
+ return render_template_string(HTML_TEMPLATE)
948
+
949
+ # Error handlers
950
+ @app.errorhandler(404)
951
+ def not_found(error):
952
+ return jsonify({'error': 'Not found'}), 404
953
+
954
+ @app.errorhandler(500)
955
+ def internal_error(error):
956
+ db.session.rollback()
957
+ return jsonify({'error': 'Internal server error'}), 500
958
+
959
+ if __name__ == '__main__':
960
+ port = int(os.environ.get('PORT', 7860))
961
+
962
+ # Use Werkzeug's built-in WSGI server with threading enabled for better production performance
963
+ from werkzeug.serving import run_simple
964
+
965
+ logger.info(f"===== Application Startup at {datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')} =====")
966
+ logger.info(f"Starting production server on port {port}")
967
+
968
+ # Run with threading enabled and proper error handling
969
+ run_simple(
970
+ hostname='0.0.0.0',
971
+ port=port,
972
+ application=app,
973
+ use_reloader=False,
974
+ use_debugger=False,
975
+ threaded=True,
976
+ processes=1
977
+ )
978
+