kuro223 commited on
Commit
afbe749
·
1 Parent(s): d41d7ab
instance/coursels.db CHANGED
@@ -1,3 +1,3 @@
1
  version https://git-lfs.github.com/spec/v1
2
- oid sha256:cc778fd2d368894b22d8b9f6cc2bd2f814f1eb45eee675f20149fe3f1c004b86
3
- size 315392
 
1
  version https://git-lfs.github.com/spec/v1
2
+ oid sha256:63df5905a95a503712def14d95e3161c838b7f28682649b42f36abc6f721b64c
3
+ size 512000
static/css/animations.css ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Additional animations and effects */
2
+ @keyframes fadeIn {
3
+ from { opacity: 0; transform: translateY(20px); }
4
+ to { opacity: 1; transform: translateY(0); }
5
+ }
6
+
7
+ @keyframes gradientAnimation {
8
+ 0% { background-position: 0% 50%; }
9
+ 50% { background-position: 100% 50%; }
10
+ 100% { background-position: 0% 50%; }
11
+ }
12
+
13
+ @keyframes pulse {
14
+ 0% { transform: scale(1); }
15
+ 50% { transform: scale(1.05); }
16
+ 100% { transform: scale(1); }
17
+ }
18
+
19
+ /* Apply animations to elements */
20
+ .header-content h1 {
21
+ background: linear-gradient(135deg, var(--primary-1), var(--accent-1), var(--primary-2));
22
+ background-size: 200% 200%;
23
+ animation: gradientAnimation 5s ease infinite;
24
+ -webkit-background-clip: text;
25
+ background-clip: text;
26
+ -webkit-text-fill-color: transparent;
27
+ }
28
+
29
+ .card {
30
+ animation: fadeIn 0.6s ease-out;
31
+ animation-fill-mode: both;
32
+ }
33
+
34
+ .cards-grid .card:nth-child(1) { animation-delay: 0.1s; }
35
+ .cards-grid .card:nth-child(2) { animation-delay: 0.2s; }
36
+ .cards-grid .card:nth-child(3) { animation-delay: 0.3s; }
37
+ .cards-grid .card:nth-child(4) { animation-delay: 0.4s; }
38
+
39
+ .btn-primary:active {
40
+ animation: pulse 0.3s ease;
41
+ }
42
+
43
+ /* Theme toggle animation */
44
+ .theme-toggle-animation {
45
+ animation: pulse 0.3s ease;
46
+ }
47
+
48
+ /* Improved glassmorphism effect */
49
+ .glassmorphism {
50
+ background: rgba(var(--bg-rgb, 255, 255, 255), 0.8);
51
+ backdrop-filter: blur(12px);
52
+ -webkit-backdrop-filter: blur(12px);
53
+ border: 1px solid rgba(var(--text-rgb, 0, 0, 0), 0.1);
54
+ }
55
+
56
+ /* Enhanced card hover effects */
57
+ .card::after {
58
+ content: '';
59
+ position: absolute;
60
+ top: 0;
61
+ left: 0;
62
+ width: 100%;
63
+ height: 100%;
64
+ background: linear-gradient(135deg,
65
+ rgba(var(--primary-rgb), 0.1),
66
+ rgba(var(--accent-rgb), 0.1));
67
+ opacity: 0;
68
+ transition: opacity var(--transition-base);
69
+ pointer-events: none;
70
+ }
71
+
72
+ .card:hover::after {
73
+ opacity: 1;
74
+ }
static/css/style.css CHANGED
@@ -1,184 +1,517 @@
1
- /* Custom styles for elegant, mobile-first design */
2
- @import url('https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3
 
4
  body {
5
- font-family: 'Roboto', sans-serif;
6
- background-color: #f8f9fa;
7
- color: #212529;
8
- line-height: 1.6;
 
 
 
 
 
 
 
 
9
  }
10
 
11
  .container {
12
- max-width: 1200px;
 
 
13
  margin: 0 auto;
14
- padding: 0 15px;
15
  }
16
 
17
  .header {
18
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
19
- color: white;
20
- padding: 1rem 0;
21
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
 
 
 
 
 
22
  }
23
 
24
  .header h1 {
25
  margin: 0;
26
  font-size: 1.5rem;
27
- font-weight: 500;
 
 
 
 
28
  }
29
 
30
  .navbar {
31
- background-color: #fff;
32
- border-bottom: 1px solid #e9ecef;
33
- padding: 0.5rem 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  }
35
 
36
- .navbar .nav-link {
37
- color: #495057;
 
 
 
 
 
 
38
  font-weight: 500;
39
- transition: color 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
- .navbar .nav-link:hover {
43
- color: #667eea;
 
 
 
 
 
 
 
 
 
 
 
44
  }
45
 
46
  .card {
47
- border: none;
48
- border-radius: 12px;
49
- box-shadow: 0 4px 6px rgba(0,0,0,0.07);
50
- transition: transform 0.3s ease, box-shadow 0.3s ease;
51
- margin-bottom: 1rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  }
53
 
54
  .card:hover {
55
- transform: translateY(-2px);
56
- box-shadow: 0 8px 25px rgba(0,0,0,0.15);
 
 
 
 
 
57
  }
58
 
59
  .card-img-top {
60
- border-radius: 12px 12px 0 0;
61
- height: 150px;
62
  object-fit: cover;
 
 
 
 
 
 
 
63
  }
64
 
65
  .card-body {
66
- padding: 1.5rem;
67
  }
68
 
69
  .card-title {
70
- font-size: 1.1rem;
71
  font-weight: 600;
72
- color: #495057;
73
- margin-bottom: 0.5rem;
 
74
  }
75
 
76
- .btn-primary {
77
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
78
- border: none;
79
- border-radius: 8px;
80
  padding: 0.75rem 1.5rem;
81
- font-weight: 500;
82
- transition: transform 0.2s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
83
  }
84
 
85
  .btn-primary:hover {
86
  transform: translateY(-1px);
87
- background: linear-gradient(135deg, #5a6fd8 0%, #6a4190 100%);
88
  }
89
 
90
  .btn-secondary {
91
- background-color: #6c757d;
92
- border: none;
93
- border-radius: 8px;
94
- padding: 0.5rem 1rem;
95
- font-weight: 500;
96
  }
97
 
98
- .footer {
99
- background-color: #343a40;
100
- color: #adb5bd;
101
- text-align: center;
102
- padding: 1rem 0;
103
- margin-top: 2rem;
104
  }
105
 
 
 
106
  .article-content {
107
- background: white;
108
- border-radius: 12px;
109
- padding: 2rem;
110
- box-shadow: 0 4px 6px rgba(0,0,0,0.07);
111
- margin-top: 1rem;
 
 
 
112
  }
113
 
114
  .article-content h1 {
115
- color: #495057;
 
 
116
  font-weight: 700;
117
- margin-bottom: 1rem;
118
- }
119
-
120
- .article-content p {
121
- margin-bottom: 1rem;
122
- }
123
-
124
- .trix-content img {
125
- width: 100%;
126
- height: auto;
127
- cursor: pointer;
128
- }
129
-
130
- .breadcrumb {
131
- background-color: transparent;
132
- padding: 0;
133
- margin-bottom: 1rem;
134
- }
135
-
136
- .breadcrumb-item a {
137
- color: #667eea;
138
- }
139
-
140
- /* Mobile optimizations */
141
- @media (max-width: 576px) {
142
- .header h1 {
143
- font-size: 1.25rem;
144
- }
145
-
146
- .card-body {
147
- padding: 1rem;
148
- }
149
-
150
- .article-content {
151
- background: none;
152
- border-radius: 0;
153
- box-shadow: none;
154
- padding: 0.5rem;
155
- }
156
-
157
- .btn {
158
- width: 100%;
159
- margin-bottom: 0.5rem;
160
- }
161
- }
162
-
163
- /* Admin specific */
164
- .admin-nav {
165
- background-color: #f8f9fa;
166
- border-radius: 8px;
167
- padding: 1rem;
168
- margin-bottom: 1rem;
169
- }
170
-
171
- .admin-nav a {
172
- display: inline-block;
173
- margin: 0.25rem;
174
- padding: 0.5rem 1rem;
175
- background-color: #667eea;
176
- color: white;
177
- text-decoration: none;
178
- border-radius: 6px;
179
- transition: background-color 0.3s ease;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  }
181
 
182
- .admin-nav a:hover {
183
- background-color: #5a6fd8;
184
  }
 
1
+ /* Modern, clean and accessible design system */
2
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
3
+
4
+ :root {
5
+ /* Light theme colors */
6
+ --bg: #ffffff;
7
+ --bg-alt: #f8fafc;
8
+ --bg-card: #ffffff;
9
+ --text: #1e293b;
10
+ --text-light: #64748b;
11
+ --primary-1: #6366f1;
12
+ --primary-2: #4f46e5;
13
+ --accent-1: #f472b6;
14
+ --accent-2: #ec4899;
15
+ --success: #10b981;
16
+ --error: #ef4444;
17
+ --border-color: rgba(0, 0, 0, 0.1);
18
+
19
+ /* Typography */
20
+ --font-base: 'Inter', system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial;
21
+ --line-height-base: 1.6;
22
+
23
+ /* Spacing */
24
+ --spacing-xs: 0.25rem;
25
+ --spacing-sm: 0.5rem;
26
+ --spacing-md: 1rem;
27
+ --spacing-lg: 1.5rem;
28
+ --spacing-xl: 2rem;
29
+
30
+ /* UI Elements */
31
+ --card-radius: 16px;
32
+ --btn-radius: 999px; /* pill buttons */
33
+ --shadow-sm: 0 1px 3px rgba(0,0,0,0.1);
34
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.07), 0 1px 3px rgba(0,0,0,0.08);
35
+ --shadow-lg: 0 10px 15px -3px rgba(0,0,0,0.1), 0 4px 6px -2px rgba(0,0,0,0.05);
36
+
37
+ /* Transitions */
38
+ --transition-fast: 150ms ease;
39
+ --transition-base: 250ms ease;
40
+ }
41
+
42
+ /* Dark theme colors */
43
+ [data-theme="dark"] {
44
+ --bg: #0f172a;
45
+ --bg-alt: #1e293b;
46
+ --bg-card: #1e293b;
47
+ --text: #e2e8f0;
48
+ --text-light: #94a3b8;
49
+ --primary-1: #818cf8;
50
+ --primary-2: #6366f1;
51
+ --accent-1: #f472b6;
52
+ --accent-2: #ec4899;
53
+ --border-color: rgba(255, 255, 255, 0.1);
54
+ }
55
+
56
+ /* Base styles with modern refinements */
57
+ html, body {
58
+ height: 100%;
59
+ scroll-behavior: smooth;
60
+ }
61
 
62
  body {
63
+ margin: 0;
64
+ font-family: var(--font-base);
65
+ background: var(--bg);
66
+ color: var(--text);
67
+ line-height: var(--line-height-base);
68
+ -webkit-font-smoothing: antialiased;
69
+ -moz-osx-font-smoothing: grayscale;
70
+ }
71
+
72
+ ::selection {
73
+ background: var(--primary-1);
74
+ color: white;
75
  }
76
 
77
  .container {
78
+ width: 100%;
79
+ max-width: 100%;
80
+ padding: 0 var(--spacing-md);
81
  margin: 0 auto;
82
+ box-sizing: border-box;
83
  }
84
 
85
  .header {
86
+ background: var(--bg);
87
+ color: var(--text);
88
+ padding: var(--spacing-md) 0;
89
+ border-bottom: 1px solid rgba(0,0,0,0.1);
90
+ position: sticky;
91
+ top: 0;
92
+ z-index: 1000;
93
+ backdrop-filter: blur(8px);
94
+ -webkit-backdrop-filter: blur(8px);
95
  }
96
 
97
  .header h1 {
98
  margin: 0;
99
  font-size: 1.5rem;
100
+ font-weight: 600;
101
+ background: linear-gradient(135deg, var(--primary-1), var(--primary-2));
102
+ -webkit-background-clip: text;
103
+ -webkit-text-fill-color: transparent;
104
+ letter-spacing: -0.02em;
105
  }
106
 
107
  .navbar {
108
+ display: flex;
109
+ align-items: center;
110
+ justify-content: space-between;
111
+ padding: var(--spacing-sm) 0;
112
+ gap: var(--spacing-md);
113
+ }
114
+
115
+ .nav-links {
116
+ display: flex;
117
+ gap: var(--spacing-md);
118
+ flex-wrap: wrap;
119
+ align-items: center;
120
+ }
121
+
122
+ /* Theme toggle button */
123
+ .theme-toggle {
124
+ background: none;
125
+ border: none;
126
+ padding: var(--spacing-sm);
127
+ cursor: pointer;
128
+ color: var(--text-light);
129
+ border-radius: 50%;
130
+ width: 40px;
131
+ height: 40px;
132
+ display: flex;
133
+ align-items: center;
134
+ justify-content: center;
135
+ transition: all var(--transition-base);
136
+ position: relative;
137
+ overflow: hidden;
138
+ }
139
+
140
+ .theme-toggle:hover {
141
+ background: var(--bg-alt);
142
+ color: var(--primary-1);
143
+ transform: scale(1.1);
144
+ }
145
+
146
+ .theme-toggle::before {
147
+ content: "🌙";
148
+ position: absolute;
149
+ opacity: 0;
150
+ transform: translateY(100%);
151
+ transition: all var(--transition-base);
152
+ }
153
+
154
+ .theme-toggle::after {
155
+ content: "☀️";
156
+ position: absolute;
157
+ opacity: 1;
158
+ transform: translateY(0);
159
+ transition: all var(--transition-base);
160
+ }
161
+
162
+ [data-theme="dark"] .theme-toggle::before {
163
+ opacity: 1;
164
+ transform: translateY(0);
165
  }
166
 
167
+ [data-theme="dark"] .theme-toggle::after {
168
+ opacity: 0;
169
+ transform: translateY(-100%);
170
+ }
171
+
172
+ .nav-links a {
173
+ color: var(--text-light);
174
+ text-decoration: none;
175
  font-weight: 500;
176
+ padding: var(--spacing-xs) var(--spacing-sm);
177
+ border-radius: var(--btn-radius);
178
+ transition: all var(--transition-fast);
179
+ }
180
+
181
+ .nav-links a:hover {
182
+ color: var(--primary-1);
183
+ background: var(--bg-alt);
184
+ }
185
+
186
+ .nav-links a[rel="noopener"]::after{
187
+ content: '↗';
188
+ display:inline-block;
189
+ margin-left:0.45rem;
190
+ font-size:0.85rem;
191
+ opacity:0.8;
192
  }
193
 
194
+ .nav-links svg {
195
+ vertical-align: middle;
196
+ display: inline-block;
197
+ margin-right: 0.15rem;
198
+ color: inherit;
199
+ }
200
+
201
+ /* Modern card grid with hover effects */
202
+ .cards-grid {
203
+ display: grid;
204
+ grid-template-columns: 1fr;
205
+ gap: var(--spacing-lg);
206
+ padding: var(--spacing-md) 0;
207
  }
208
 
209
  .card {
210
+ background: var(--bg-card);
211
+ border-radius: var(--card-radius);
212
+ box-shadow: var(--shadow-md);
213
+ overflow: hidden;
214
+ transition: all var(--transition-base);
215
+ border: 1px solid var(--border-color);
216
+ position: relative;
217
+ z-index: 1;
218
+ }
219
+
220
+ .card::before {
221
+ content: '';
222
+ position: absolute;
223
+ top: 0;
224
+ left: 0;
225
+ width: 100%;
226
+ height: 100%;
227
+ background: linear-gradient(135deg, var(--primary-1), var(--accent-1));
228
+ opacity: 0;
229
+ transition: opacity var(--transition-base);
230
+ z-index: -1;
231
+ border-radius: var(--card-radius);
232
  }
233
 
234
  .card:hover {
235
+ transform: translateY(-4px) scale(1.02);
236
+ box-shadow: var(--shadow-lg);
237
+ border-color: transparent;
238
+ }
239
+
240
+ .card:hover::before {
241
+ opacity: 0.1;
242
  }
243
 
244
  .card-img-top {
245
+ width: 100%;
246
+ height: 220px;
247
  object-fit: cover;
248
+ transition: all var(--transition-base);
249
+ filter: brightness(1);
250
+ }
251
+
252
+ .card:hover .card-img-top {
253
+ transform: scale(1.05);
254
+ filter: brightness(1.1);
255
  }
256
 
257
  .card-body {
258
+ padding: var(--spacing-lg);
259
  }
260
 
261
  .card-title {
262
+ margin: 0 0 var(--spacing-sm);
263
  font-weight: 600;
264
+ color: var(--text);
265
+ font-size: 1.25rem;
266
+ line-height: 1.4;
267
  }
268
 
269
+ .btn {
270
+ display: inline-flex;
271
+ align-items: center;
272
+ justify-content: center;
273
  padding: 0.75rem 1.5rem;
274
+ border-radius: var(--btn-radius);
275
+ font-weight: 600;
276
+ text-align: center;
277
+ transition: all var(--transition-fast);
278
+ text-decoration: none;
279
+ border: 0;
280
+ cursor: pointer;
281
+ gap: var(--spacing-sm);
282
+ font-size: 0.95rem;
283
+ /* ensure pill appearance on all buttons */
284
+ border-radius: 999px;
285
+ }
286
+
287
+ .btn-primary {
288
+ background: linear-gradient(135deg, var(--primary-1), var(--primary-2));
289
+ color: white;
290
+ box-shadow: 0 2px 4px rgba(59, 130, 246, 0.25);
291
  }
292
 
293
  .btn-primary:hover {
294
  transform: translateY(-1px);
295
+ box-shadow: 0 4px 8px rgba(59, 130, 246, 0.35);
296
  }
297
 
298
  .btn-secondary {
299
+ background: var(--bg-alt);
300
+ color: var(--text);
301
+ border: 1px solid rgba(0,0,0,0.1);
 
 
302
  }
303
 
304
+ .btn-secondary:hover {
305
+ background: var(--bg);
306
+ border-color: var(--primary-1);
307
+ color: var(--primary-1);
 
 
308
  }
309
 
310
+ /* Footer removed - styles cleaned up */
311
+
312
  .article-content {
313
+ background: var(--bg);
314
+ border-radius: var(--card-radius);
315
+ padding: var(--spacing-xl);
316
+ margin-top: var(--spacing-lg);
317
+ box-shadow: var(--shadow-md);
318
+ max-width: 800px;
319
+ margin-left: auto;
320
+ margin-right: auto;
321
  }
322
 
323
  .article-content h1 {
324
+ font-size: 2rem;
325
+ margin: var(--spacing-md) 0 var(--spacing-lg);
326
+ color: var(--text);
327
  font-weight: 700;
328
+ line-height: 1.3;
329
+ }
330
+
331
+ .article-content p {
332
+ margin: 0 0 var(--spacing-md);
333
+ color: var(--text-light);
334
+ line-height: 1.7;
335
+ }
336
+
337
+ .trix-content img{max-width:100%;height:auto;cursor:pointer;border-radius:6px}
338
+
339
+ .breadcrumb{background:transparent;padding:0;margin-bottom:1rem}
340
+ .breadcrumb{
341
+ display:flex;
342
+ flex-wrap:wrap;
343
+ gap:0.5rem;
344
+ align-items:center;
345
+ background:transparent;
346
+ padding:0;
347
+ margin-bottom:1rem;
348
+ }
349
+
350
+ .breadcrumb-item{
351
+ list-style:none;
352
+ }
353
+
354
+ .breadcrumb-item a{
355
+ display:inline-block;
356
+ padding:0.4rem 0.8rem;
357
+ background: var(--bg-alt);
358
+ color: var(--text);
359
+ border-radius: 999px;
360
+ text-decoration:none;
361
+ font-weight:600;
362
+ transition: all var(--transition-fast);
363
+ border: 1px solid var(--border-color);
364
+ }
365
+
366
+ .breadcrumb-item a:hover{
367
+ transform: translateY(-2px);
368
+ box-shadow: var(--shadow-sm);
369
+ }
370
+
371
+ .breadcrumb-item.active{
372
+ display:inline-block;
373
+ padding:0.45rem 1rem;
374
+ border-radius:999px;
375
+ background: linear-gradient(90deg, var(--primary-1), var(--accent-1));
376
+ color: #fff;
377
+ font-weight:700;
378
+ border: none;
379
+ }
380
+
381
+ /* small separator arrow */
382
+ .breadcrumb-item + .breadcrumb-item::before{
383
+ content: '›';
384
+ margin: 0 0.35rem;
385
+ color: var(--text-light);
386
+ }
387
+
388
+ /* floats for image shortcodes */
389
+ .float-left{float:left;margin:0 1rem 1rem 0;max-width:40%}
390
+ .float-right{float:right;margin:0 0 1rem 1rem;max-width:40%}
391
+
392
+ /* video responsive container */
393
+ .video-player{width:100%;aspect-ratio:16/9;overflow:hidden;border-radius:8px;margin-bottom:1rem}
394
+ .video-player iframe{width:100%;height:100%;border:0}
395
+
396
+ /* admin navigation */
397
+ .admin-nav{background:transparent;padding:.5rem 0;margin-bottom:1rem}
398
+ .admin-nav a{display:inline-block;margin:.25rem;padding:.5rem .75rem;border-radius:6px;background:linear-gradient(135deg,var(--primary-1),var(--primary-2));color:#fff;text-decoration:none}
399
+
400
+ /* Buttons full width on small screens */
401
+ .btn-full{display:block;width:100%}
402
+
403
+ /* small utilities */
404
+ .text-muted{color:var(--muted)}
405
+
406
+ /* Responsive layouts */
407
+ @media (min-width: 576px) {
408
+ .container {
409
+ max-width: 540px;
410
+ }
411
+ .header h1 {
412
+ font-size: 1.75rem;
413
+ }
414
+ }
415
+
416
+ @media (min-width: 768px) {
417
+ .container {
418
+ max-width: 720px;
419
+ }
420
+ .cards-grid {
421
+ grid-template-columns: repeat(2, 1fr);
422
+ }
423
+ .article-content {
424
+ padding: var(--spacing-xl);
425
+ margin-top: var(--spacing-xl);
426
+ }
427
+ .btn {
428
+ display: inline-flex;
429
+ width: auto;
430
+ }
431
+ }
432
+
433
+ @media (min-width: 992px) {
434
+ .container {
435
+ max-width: 960px;
436
+ }
437
+ .cards-grid {
438
+ grid-template-columns: repeat(3, 1fr);
439
+ gap: var(--spacing-xl);
440
+ }
441
+ .header {
442
+ padding: var(--spacing-lg) 0;
443
+ }
444
+ }
445
+
446
+ @media (min-width: 1200px) {
447
+ .container {
448
+ max-width: 1140px;
449
+ }
450
+ .article-content {
451
+ padding: calc(var(--spacing-xl) * 1.5);
452
+ }
453
+ }
454
+
455
+ /* Enhanced Trix editor */
456
+ trix-editor {
457
+ min-height: 280px;
458
+ border-radius: var(--card-radius);
459
+ border: 1px solid rgba(0,0,0,0.1);
460
+ padding: var(--spacing-md);
461
+ background: var(--bg);
462
+ color: var(--text);
463
+ font-family: var(--font-base);
464
+ line-height: var(--line-height-base);
465
+ transition: border-color var(--transition-fast);
466
+ }
467
+
468
+ trix-editor:focus {
469
+ outline: none;
470
+ border-color: var(--primary-1);
471
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.15);
472
+ }
473
+
474
+ /* Small screen optimizations */
475
+ @media (max-width: 575.98px) {
476
+ .card-img-top {
477
+ height: 200px;
478
+ }
479
+ .btn {
480
+ width: 100%;
481
+ margin-bottom: var(--spacing-sm);
482
+ }
483
+ .article-content {
484
+ padding: var(--spacing-md);
485
+ border-radius: var(--card-radius);
486
+ margin-top: var(--spacing-md);
487
+ }
488
+ .nav-links {
489
+ gap: var(--spacing-xs);
490
+ }
491
+ .header h1 {
492
+ font-size: 1.25rem;
493
+ }
494
+ }
495
+
496
+ /* Modern utilities */
497
+ .text-gradient {
498
+ background: linear-gradient(135deg, var(--primary-1), var(--primary-2));
499
+ -webkit-background-clip: text;
500
+ background-clip: text;
501
+ -webkit-text-fill-color: transparent;
502
+ }
503
+
504
+ .glassmorphism {
505
+ background: rgba(255, 255, 255, 0.8);
506
+ backdrop-filter: blur(8px);
507
+ -webkit-backdrop-filter: blur(8px);
508
+ border: 1px solid rgba(255, 255, 255, 0.3);
509
+ }
510
+
511
+ .shadow-hover {
512
+ transition: box-shadow var(--transition-fast);
513
  }
514
 
515
+ .shadow-hover:hover {
516
+ box-shadow: var(--shadow-lg);
517
  }
static/js/theme.js ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ document.addEventListener('DOMContentLoaded', () => {
2
+ // Respect any early-applied theme (set inline in head) to avoid flicker
3
+ const initialTheme = document.documentElement.getAttribute('data-theme') || localStorage.getItem('theme') || 'light';
4
+ document.documentElement.setAttribute('data-theme', initialTheme);
5
+
6
+ // Avoid creating duplicate toggle if one already exists
7
+ if (document.querySelector('.theme-toggle')) return;
8
+
9
+ // Create and insert theme toggle button
10
+ const themeToggle = document.createElement('button');
11
+ themeToggle.className = 'theme-toggle';
12
+ themeToggle.setAttribute('aria-label', 'Toggle dark mode');
13
+
14
+ // Set initial accessible label/title/icon state
15
+ themeToggle.title = initialTheme === 'dark' ? 'Mode clair' : 'Mode sombre';
16
+
17
+ // Find the nav-links container and insert the button
18
+ const navLinks = document.querySelector('.nav-links');
19
+ if (navLinks) {
20
+ navLinks.appendChild(themeToggle);
21
+ }
22
+
23
+ // Toggle theme function
24
+ const toggleTheme = () => {
25
+ const currentTheme = document.documentElement.getAttribute('data-theme') || 'light';
26
+ const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
27
+
28
+ document.documentElement.setAttribute('data-theme', newTheme);
29
+ localStorage.setItem('theme', newTheme);
30
+ themeToggle.title = newTheme === 'dark' ? 'Mode clair' : 'Mode sombre';
31
+
32
+ // Add animation class
33
+ themeToggle.classList.add('theme-toggle-animation');
34
+ setTimeout(() => themeToggle.classList.remove('theme-toggle-animation'), 300);
35
+ };
36
+
37
+ // Add click event listener
38
+ themeToggle.addEventListener('click', toggleTheme);
39
+ });
templates/admin_edit_article.html CHANGED
@@ -1,97 +1,419 @@
1
- <!DOCTYPE html>
2
- <html lang="fr">
3
- <head>
4
- <meta charset="UTF-8">
5
- <title>Éditer Article</title>
6
- <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
7
- <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
8
- <style>
9
- body { font-family: Arial, sans-serif; margin: 20px; }
10
- form { max-width: 900px; margin:0 auto }
11
- label { display: block; margin-top: 10px; }
12
- input, select, button { width: 100%; padding: 10px; margin-top: 5px; box-sizing: border-box }
13
- trix-editor { border: 1px solid #e9ecef; min-height: 300px; border-radius:8px }
14
- .article-preview { background:#fff;padding:1rem;border-radius:8px;box-shadow:0 4px 12px rgba(0,0,0,0.06);margin-top:1rem }
15
- </style>
16
- </head>
17
- <body>
18
- <h1>Éditer Article</h1>
19
- <a href="{{ url_for('admin_articles') }}">Retour aux articles</a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  <form method="post" enctype="multipart/form-data">
21
- <label for="title">Titre:</label>
22
- <input type="text" id="title" name="title" value="{{ article.title }}" required>
 
 
23
 
24
- <label for="category_id">Catégorie:</label>
25
- <select id="category_id" name="category_id" required>
26
- {% for category in article.category.subject.categories %}
27
- <option value="{{ category.id }}" {% if category.id == article.category_id %}selected{% endif %}>{{ category.name }}</option>
28
- {% endfor %}
29
- </select>
 
 
30
 
31
- <label for="icon">Image (fichier image):</label>
32
- <input type="file" id="icon" name="icon" accept="image/*">
 
 
 
 
 
 
 
 
 
 
 
 
33
 
34
- <label for="youtube_url">Lien YouTube (optionnel) :</label>
35
- <input type="url" id="youtube_url" name="youtube_url" placeholder="https://www.youtube.com/watch?v=..." value="{{ article.youtube_url or '' }}">
 
 
 
 
36
 
37
- <label for="content">Contenu:</label>
38
- <input id="content" type="hidden" name="content" value="{{ article.content | safe }}">
39
- <trix-editor input="content"></trix-editor>
 
 
40
 
41
- <button type="submit">Enregistrer</button>
 
 
42
  </form>
43
 
44
- <h3>Aperçu</h3>
45
- <div id="editor-preview" class="article-preview"></div>
 
 
 
 
46
 
47
- <script>
 
 
 
 
 
 
48
  // Configure Trix before init
49
- document.addEventListener('trix-before-initialize', function(){
50
- if (window.Trix && Trix.config && Trix.config.dompurify){
51
- Trix.config.dompurify.ADD_TAGS = ["iframe","video","source","table","tr","td","th","img"];
52
- Trix.config.dompurify.ADD_ATTR = ["src","controls","width","height","class","style","allowfullscreen"];
53
  }
54
- Trix.config.toolbar.getDefaultHTML = function(){
55
  return `
56
  <div class="trix-button-row">
57
- <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
58
- <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" title="Gras">Gras</button>
59
- <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" title="Italique">Italique</button>
60
- <button type="button" class="trix-button" data-trix-action="x-heading" title="Titre">H</button>
61
- <button type="button" class="trix-button" data-trix-action="x-quote" title="Citation">"</button>
62
- </span>
 
 
 
 
 
63
  </div>`;
64
  };
65
  });
66
 
67
- function renderShortcodesForPreview(html){
68
- if(!html) return '';
69
- html = html.replace(/\[youtube:([^\]]+)\]/g, function(_, url){
70
- try{ var parsed = new URL(url.trim()); var id=null; if(parsed.hostname.includes('youtu.be')) id = parsed.pathname.slice(1); else if(parsed.hostname.includes('youtube')) id = parsed.searchParams.get('v') || parsed.pathname.split('/').pop(); if(!id) return '<pre>'+url+'</pre>'; return '<div class="video-player"><iframe src="https://www.youtube.com/embed/'+id+'" allowfullscreen></iframe></div>'; }catch(e){return '<pre>'+url+'</pre>'}
 
 
 
 
 
 
 
 
 
71
  });
72
- html = html.replace(/\[image:([^\]|]+)(?:\|([^\]]+))?\]/g, function(_, url, opts){
73
- var classes=''; var style=''; if(opts){ opts.split(/[|,]/).forEach(function(o){ o=o.trim(); if(/^(left|right|center)$/.test(o)){ if(o==='left') classes='float-left'; if(o==='right') classes='float-right'; if(o==='center') style='display:block;margin-left:auto;margin-right:auto'; } else if(/^width=/.test(o)){ style += (style?';':'') + 'width:' + o.split('=')[1]; } else if(/^height=/.test(o)){ style += (style?';':'') + 'height:' + o.split('=')[1]; } }); }
74
- return '<img src="'+url.trim()+'" class="'+classes+'" style="'+style+'"/>';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  });
76
  return html;
77
  }
78
 
79
- function uploadFileToServer(fileBlob, filename, attachment){
80
- var formData = new FormData(); formData.append('file', fileBlob, filename);
81
- fetch('/upload',{method:'POST',body:formData}).then(function(r){return r.json()}).then(function(data){ if(data.url) attachment.setAttributes({url: data.url}); }).catch(function(e){console.error('Upload failed',e)});
 
 
 
 
 
 
 
 
 
 
 
 
82
  }
83
 
84
- document.addEventListener('trix-attachment-add', function(event){
85
- var attachment = event.attachment; var file = attachment.file; if(!file) return; if(!file.type.startsWith('image/')){ uploadFileToServer(file,file.name,attachment); return; }
86
- var choice = prompt("Redimensionner l'image ? Entrez largeur en px (ex:800) ou % (ex:80%). Laisser vide pour taille originale. Annuler pour ne pas uploader."); if(choice===null) return; if(!choice){ uploadFileToServer(file,file.name,attachment); return; }
87
- var reader = new FileReader(); reader.onload = function(e){ var img=new Image(); img.onload=function(){ var origW=img.naturalWidth,origH=img.naturalHeight; var targetW=origW; var c=choice.toString().trim(); if(c.endsWith('%')){ var p=parseFloat(c.replace('%','')); if(!isNaN(p)) targetW=Math.round(origW*p/100);} else { var px=parseInt(c,10); if(!isNaN(px)) targetW=px;} if(targetW<=0||targetW===origW){ uploadFileToServer(file,file.name,attachment); return; } var scale=targetW/origW,targetH=Math.round(origH*scale); var canvas=document.createElement('canvas'); canvas.width=targetW; canvas.height=targetH; var ctx=canvas.getContext('2d'); ctx.drawImage(img,0,0,targetW,targetH); canvas.toBlob(function(blob){ uploadFileToServer(blob,file.name,attachment); }, file.type||'image/jpeg', 0.92); }; img.src=e.target.result; }; reader.readAsDataURL(file);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  });
89
 
90
- document.addEventListener('trix-action-invoke', function(event){ var action = event.actionName || event.detail && event.detail.actionName; if(action==='x-align-left' || action==='x-align-right'){ var target=event.target||event.detail&&event.detail.target; if(!target) return; var editor = target.editor; var selectedRange = editor.getSelectedRange(); var doc = editor.getDocument(); var attachments = doc.getAttachments(); attachments.forEach(function(att){ var range = att.getRange(); if(selectedRange[0] <= range[0] && range[1] <= selectedRange[1]) att.setAttribute('class', action==='x-align-left'?'float-left':'float-right'); }); } });
 
 
 
 
 
 
 
91
 
92
- document.addEventListener('trix-change', function(){ var contentInput = document.querySelector('input[name="content"]'); var preview = document.getElementById('editor-preview'); if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value); });
 
93
 
94
- document.addEventListener('DOMContentLoaded', function(){ var contentInput = document.querySelector('input[name="content"]'); var preview = document.getElementById('editor-preview'); if(preview && contentInput) preview.innerHTML = renderShortcodesForPreview(contentInput.value); });
95
- </script>
96
- </body>
97
- </html>
 
 
 
 
 
 
1
+ {% extends "base.html" %}
2
+
3
+ {% block title %}Éditer Article{% endblock %}
4
+
5
+ {% block extra_head %}
6
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/trix@2.0.8/dist/trix.css">
7
+ <style>
8
+ /* Admin Editor Styles */
9
+ .admin-editor {
10
+ max-width: 1000px;
11
+ margin: 0 auto;
12
+ padding: var(--spacing-lg);
13
+ background: var(--bg-card);
14
+ border-radius: var(--card-radius);
15
+ box-shadow: var(--shadow-md);
16
+ }
17
+
18
+ .editor-header {
19
+ display: flex;
20
+ align-items: center;
21
+ justify-content: space-between;
22
+ margin-bottom: var(--spacing-lg);
23
+ flex-wrap: wrap;
24
+ gap: var(--spacing-md);
25
+ }
26
+
27
+ .editor-title {
28
+ margin: 0;
29
+ font-size: 1.5rem;
30
+ font-weight: 600;
31
+ background: linear-gradient(135deg, var(--primary-1), var(--accent-1));
32
+ -webkit-background-clip: text;
33
+ background-clip: text;
34
+ -webkit-text-fill-color: transparent;
35
+ }
36
+
37
+ .form-group {
38
+ margin-bottom: var(--spacing-md);
39
+ }
40
+
41
+ .form-group label {
42
+ display: block;
43
+ margin-bottom: var(--spacing-xs);
44
+ color: var(--text);
45
+ font-weight: 500;
46
+ }
47
+
48
+ .form-control {
49
+ width: 100%;
50
+ padding: 0.75rem 1rem;
51
+ border: 1px solid var(--border-color);
52
+ border-radius: var(--btn-radius);
53
+ background: var(--bg);
54
+ color: var(--text);
55
+ transition: all var(--transition-fast);
56
+ }
57
+
58
+ .form-control:focus {
59
+ outline: none;
60
+ border-color: var(--primary-1);
61
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
62
+ }
63
+
64
+ trix-editor {
65
+ min-height: 300px;
66
+ border: 1px solid var(--border-color);
67
+ border-radius: var(--btn-radius);
68
+ background: var(--bg);
69
+ color: var(--text);
70
+ padding: var(--spacing-md);
71
+ margin-bottom: var(--spacing-lg);
72
+ }
73
+
74
+ trix-editor:focus {
75
+ outline: none;
76
+ border-color: var(--primary-1);
77
+ box-shadow: 0 0 0 3px rgba(99, 102, 241, 0.1);
78
+ }
79
+
80
+ .preview-section {
81
+ margin-top: var(--spacing-xl);
82
+ padding: var(--spacing-lg);
83
+ background: var(--bg-alt);
84
+ border-radius: var(--card-radius);
85
+ }
86
+
87
+ .preview-header {
88
+ margin-bottom: var(--spacing-md);
89
+ padding-bottom: var(--spacing-sm);
90
+ border-bottom: 1px solid var(--border-color);
91
+ }
92
+
93
+ .preview-content {
94
+ background: var(--bg);
95
+ padding: var(--spacing-lg);
96
+ border-radius: var(--card-radius);
97
+ box-shadow: var(--shadow-sm);
98
+ }
99
+
100
+ /* Custom file input */
101
+ .file-input-wrapper {
102
+ position: relative;
103
+ }
104
+
105
+ .file-input-wrapper input[type="file"] {
106
+ opacity: 0;
107
+ position: absolute;
108
+ top: 0;
109
+ left: 0;
110
+ width: 100%;
111
+ height: 100%;
112
+ cursor: pointer;
113
+ }
114
+
115
+ .file-input-trigger {
116
+ display: block;
117
+ padding: 0.75rem 1rem;
118
+ border: 2px dashed var(--border-color);
119
+ border-radius: var(--btn-radius);
120
+ text-align: center;
121
+ color: var(--text-light);
122
+ transition: all var(--transition-fast);
123
+ }
124
+
125
+ .file-input-wrapper:hover .file-input-trigger {
126
+ border-color: var(--primary-1);
127
+ color: var(--primary-1);
128
+ }
129
+ </style>
130
+ {% endblock %}
131
+
132
+ {% block content %}
133
+ <div class="admin-editor">
134
+ <div class="editor-header">
135
+ <h1 class="editor-title">Éditer Article</h1>
136
+ <a href="{{ url_for('admin_articles') }}" class="btn btn-secondary">← Retour aux articles</a>
137
+ </div>
138
  <form method="post" enctype="multipart/form-data">
139
+ <div class="form-group">
140
+ <label for="title">Titre:</label>
141
+ <input type="text" class="form-control" id="title" name="title" value="{{ article.title }}" required>
142
+ </div>
143
 
144
+ <div class="form-group">
145
+ <label for="category_id">Catégorie:</label>
146
+ <select class="form-control" id="category_id" name="category_id" required>
147
+ {% for category in article.category.subject.categories %}
148
+ <option value="{{ category.id }}" {% if category.id == article.category_id %}selected{% endif %}>{{ category.name }}</option>
149
+ {% endfor %}
150
+ </select>
151
+ </div>
152
 
153
+ <div class="form-group">
154
+ <label for="icon">Image:</label>
155
+ <div class="file-input-wrapper">
156
+ <div class="file-input-trigger">
157
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="upload-icon">
158
+ <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
159
+ <polyline points="17 8 12 3 7 8"/>
160
+ <line x1="12" y1="3" x2="12" y2="15"/>
161
+ </svg>
162
+ <span>Glissez une image ou cliquez pour sélectionner</span>
163
+ </div>
164
+ <input type="file" id="icon" name="icon" accept="image/*">
165
+ </div>
166
+ </div>
167
 
168
+ <div class="form-group">
169
+ <label for="youtube_url">Lien YouTube (optionnel) :</label>
170
+ <input type="url" class="form-control" id="youtube_url" name="youtube_url"
171
+ placeholder="https://www.youtube.com/watch?v=..."
172
+ value="{{ article.youtube_url or '' }}">
173
+ </div>
174
 
175
+ <div class="form-group">
176
+ <label for="content">Contenu:</label>
177
+ <input id="content" type="hidden" name="content" value="{{ article.content | safe }}">
178
+ <trix-editor input="content"></trix-editor>
179
+ </div>
180
 
181
+ <div class="form-group">
182
+ <button type="submit" class="btn btn-primary">Enregistrer les modifications</button>
183
+ </div>
184
  </form>
185
 
186
+ <div class="preview-section">
187
+ <div class="preview-header">
188
+ <h3>Aperçu en direct</h3>
189
+ </div>
190
+ <div id="editor-preview" class="preview-content"></div>
191
+ </div>
192
 
193
+ </div>
194
+
195
+ {% endblock %}
196
+
197
+ {% block extra_scripts %}
198
+ <script type="text/javascript" src="https://unpkg.com/trix@2.0.8/dist/trix.umd.min.js"></script>
199
+ <script>
200
  // Configure Trix before init
201
+ document.addEventListener('trix-before-initialize', function() {
202
+ if (window.Trix && Trix.config && Trix.config.dompurify) {
203
+ Trix.config.dompurify.ADD_TAGS = ["iframe", "video", "source", "table", "tr", "td", "th", "img"];
204
+ Trix.config.dompurify.ADD_ATTR = ["src", "controls", "width", "height", "class", "style", "allowfullscreen"];
205
  }
206
+ Trix.config.toolbar.getDefaultHTML = function() {
207
  return `
208
  <div class="trix-button-row">
209
+ <span class="trix-button-group trix-button-group--text-tools" data-trix-button-group="text-tools">
210
+ <button type="button" class="trix-button trix-button--icon trix-button--icon-bold" data-trix-attribute="bold" title="Gras">Gras</button>
211
+ <button type="button" class="trix-button trix-button--icon trix-button--icon-italic" data-trix-attribute="italic" title="Italique">Italique</button>
212
+ <button type="button" class="trix-button" data-trix-action="x-heading" title="Titre">H</button>
213
+ <button type="button" class="trix-button" data-trix-action="x-quote" title="Citation">"</button>
214
+ <button type="button" class="trix-button trix-button--icon trix-button--icon-link" data-trix-attribute="href" title="Lien">Lien</button>
215
+ </span>
216
+ <span class="trix-button-group">
217
+ <button type="button" class="trix-button" data-trix-action="x-align-left" title="Aligner à gauche">←</button>
218
+ <button type="button" class="trix-button" data-trix-action="x-align-right" title="Aligner à droite">→</button>
219
+ </span>
220
  </div>`;
221
  };
222
  });
223
 
224
+ function renderShortcodesForPreview(html) {
225
+ if (!html) return '';
226
+ html = html.replace(/\[youtube:([^\]]+)\]/g, function(_, url) {
227
+ try {
228
+ var parsed = new URL(url.trim());
229
+ var id = null;
230
+ if (parsed.hostname.includes('youtu.be')) id = parsed.pathname.slice(1);
231
+ else if (parsed.hostname.includes('youtube')) id = parsed.searchParams.get('v') || parsed.pathname.split('/').pop();
232
+ if (!id) return '<pre>' + url + '</pre>';
233
+ return '<div class="video-player"><iframe src="https://www.youtube.com/embed/' + id + '" allowfullscreen></iframe></div>';
234
+ } catch (e) {
235
+ return '<pre>' + url + '</pre>'
236
+ }
237
  });
238
+ html = html.replace(/\[image:([^\]|]+)(?:\|([^\]]+))?\]/g, function(_, url, opts) {
239
+ var classes = '';
240
+ var style = '';
241
+ if (opts) {
242
+ opts.split(/[|,]/).forEach(function(o) {
243
+ o = o.trim();
244
+ if (/^(left|right|center)$/.test(o)) {
245
+ if (o === 'left') classes = 'float-left';
246
+ if (o === 'right') classes = 'float-right';
247
+ if (o === 'center') style = 'display:block;margin-left:auto;margin-right:auto';
248
+ } else if (/^width=/.test(o)) {
249
+ style += (style ? ';' : '') + 'width:' + o.split('=')[1];
250
+ } else if (/^height=/.test(o)) {
251
+ style += (style ? ';' : '') + 'height:' + o.split('=')[1];
252
+ }
253
+ });
254
+ }
255
+ return '<img src="' + url.trim() + '" class="' + classes + '" style="' + style + '"/>';
256
  });
257
  return html;
258
  }
259
 
260
+ function uploadFileToServer(fileBlob, filename, attachment) {
261
+ var formData = new FormData();
262
+ formData.append('file', fileBlob, filename);
263
+ fetch('/upload', {
264
+ method: 'POST',
265
+ body: formData
266
+ }).then(function(r) {
267
+ return r.json()
268
+ }).then(function(data) {
269
+ if (data.url) attachment.setAttributes({
270
+ url: data.url
271
+ });
272
+ }).catch(function(e) {
273
+ console.error('Upload failed', e)
274
+ });
275
  }
276
 
277
+ // File upload and image resize handling
278
+ document.addEventListener('trix-attachment-add', function(event) {
279
+ var attachment = event.attachment;
280
+ var file = attachment.file;
281
+ if (!file) return;
282
+ if (!file.type.startsWith('image/')) {
283
+ uploadFileToServer(file, file.name, attachment);
284
+ return;
285
+ }
286
+
287
+ // Create modal for image resize
288
+ const modal = document.createElement('div');
289
+ modal.style.cssText = `
290
+ position: fixed;
291
+ top: 0;
292
+ left: 0;
293
+ right: 0;
294
+ bottom: 0;
295
+ background: rgba(0,0,0,0.5);
296
+ display: flex;
297
+ align-items: center;
298
+ justify-content: center;
299
+ z-index: 9999;
300
+ `;
301
+
302
+ const modalContent = document.createElement('div');
303
+ modalContent.style.cssText = `
304
+ background: var(--bg);
305
+ padding: 2rem;
306
+ border-radius: var(--card-radius);
307
+ box-shadow: var(--shadow-lg);
308
+ max-width: 400px;
309
+ width: 90%;
310
+ `;
311
+
312
+ modalContent.innerHTML = `
313
+ <h3 style="margin-top:0">Redimensionner l'image ?</h3>
314
+ <p>Entrez la largeur souhaitée :</p>
315
+ <input type="text" placeholder="800px ou 80%" style="width:100%;padding:0.5rem;margin:1rem 0;border-radius:var(--btn-radius);border:1px solid var(--border-color)">
316
+ <div style="display:flex;gap:1rem;justify-content:flex-end">
317
+ <button class="btn btn-secondary cancel">Annuler</button>
318
+ <button class="btn btn-primary confirm">Confirmer</button>
319
+ </div>
320
+ `;
321
+
322
+ modal.appendChild(modalContent);
323
+ document.body.appendChild(modal);
324
+
325
+ const input = modalContent.querySelector('input');
326
+ const cancelBtn = modalContent.querySelector('.cancel');
327
+ const confirmBtn = modalContent.querySelector('.confirm');
328
+
329
+ cancelBtn.onclick = () => {
330
+ document.body.removeChild(modal);
331
+ };
332
+
333
+ confirmBtn.onclick = () => {
334
+ const choice = input.value;
335
+ document.body.removeChild(modal);
336
+
337
+ if (!choice) {
338
+ uploadFileToServer(file, file.name, attachment);
339
+ return;
340
+ }
341
+
342
+ const reader = new FileReader();
343
+ reader.onload = function(e) {
344
+ const img = new Image();
345
+ img.onload = function() {
346
+ const origW = img.naturalWidth,
347
+ origH = img.naturalHeight;
348
+ let targetW = origW;
349
+ const c = choice.toString().trim();
350
+
351
+ if (c.endsWith('%')) {
352
+ const p = parseFloat(c.replace('%', ''));
353
+ if (!isNaN(p)) targetW = Math.round(origW * p / 100);
354
+ } else {
355
+ const px = parseInt(c, 10);
356
+ if (!isNaN(px)) targetW = px;
357
+ }
358
+
359
+ if (targetW <= 0 || targetW === origW) {
360
+ uploadFileToServer(file, file.name, attachment);
361
+ return;
362
+ }
363
+
364
+ const scale = targetW / origW,
365
+ targetH = Math.round(origH * scale);
366
+ const canvas = document.createElement('canvas');
367
+ canvas.width = targetW;
368
+ canvas.height = targetH;
369
+ const ctx = canvas.getContext('2d');
370
+ ctx.drawImage(img, 0, 0, targetW, targetH);
371
+ canvas.toBlob(function(blob) {
372
+ uploadFileToServer(blob, file.name, attachment);
373
+ }, file.type || 'image/jpeg', 0.92);
374
+ };
375
+ img.src = e.target.result;
376
+ };
377
+ reader.readAsDataURL(file);
378
+ };
379
+ });
380
+
381
+ // Handle image alignment
382
+ document.addEventListener('trix-action-invoke', function(event) {
383
+ var action = event.actionName || event.detail && event.detail.actionName;
384
+ if (action === 'x-align-left' || action === 'x-align-right') {
385
+ var target = event.target || event.detail && event.detail.target;
386
+ if (!target) return;
387
+ var editor = target.editor;
388
+ var selectedRange = editor.getSelectedRange();
389
+ var doc = editor.getDocument();
390
+ var attachments = doc.getAttachments();
391
+ attachments.forEach(function(att) {
392
+ var range = att.getRange();
393
+ if (selectedRange[0] <= range[0] && range[1] <= selectedRange[1])
394
+ att.setAttribute('class', action === 'x-align-left' ? 'float-left' : 'float-right');
395
+ });
396
+ }
397
  });
398
 
399
+ // Live preview updates
400
+ function updatePreview() {
401
+ var contentInput = document.querySelector('input[name="content"]');
402
+ var preview = document.getElementById('editor-preview');
403
+ if (preview && contentInput) {
404
+ preview.innerHTML = renderShortcodesForPreview(contentInput.value);
405
+ }
406
+ }
407
 
408
+ document.addEventListener('trix-change', updatePreview);
409
+ document.addEventListener('DOMContentLoaded', updatePreview);
410
 
411
+ // File input visual feedback
412
+ document.querySelector('.file-input-wrapper input[type="file"]').addEventListener('change', function(e) {
413
+ const trigger = this.parentElement.querySelector('.file-input-trigger');
414
+ if (this.files.length > 0) {
415
+ trigger.textContent = `Fichier sélectionné : ${this.files[0].name}`;
416
+ }
417
+ });
418
+ </script>
419
+ {% endblock %}
templates/base.html CHANGED
@@ -2,18 +2,31 @@
2
  <html lang="fr">
3
  <head>
4
  <meta charset="UTF-8">
 
 
 
 
 
 
 
 
 
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
  <title>{% block title %}Mariam AI{% endblock %}</title>
7
  <!-- Bootstrap CSS -->
8
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
9
  <!-- Custom CSS -->
10
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 
 
11
  {% block extra_head %}{% endblock %}
12
  </head>
13
  <body>
14
- <header class="header">
15
  <div class="container">
16
- <h1>Mariam AI</h1>
 
 
17
  </div>
18
  </header>
19
 
@@ -21,7 +34,15 @@
21
  <div class="container">
22
  <ul class="nav nav-links">
23
  <li class="nav-item">
24
- <a class="nav-link" href="{{ url_for('home') }}">Accueil</a>
 
 
 
 
 
 
 
 
25
  </li>
26
  {% if request.endpoint and 'admin' in request.endpoint %}
27
  <li class="nav-item">
@@ -38,8 +59,11 @@
38
 
39
 
40
 
 
 
41
  <!-- Bootstrap JS -->
42
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
 
43
  {% block extra_scripts %}{% endblock %}
44
  </body>
45
  </html>
 
2
  <html lang="fr">
3
  <head>
4
  <meta charset="UTF-8">
5
+ <script>
6
+ // Apply saved theme as early as possible to avoid FOUC (flash of un-themed content)
7
+ (function(){
8
+ try {
9
+ var t = localStorage.getItem('theme');
10
+ if (t) document.documentElement.setAttribute('data-theme', t);
11
+ } catch(e) { /* ignore */ }
12
+ })();
13
+ </script>
14
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
15
  <title>{% block title %}Mariam AI{% endblock %}</title>
16
  <!-- Bootstrap CSS -->
17
  <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
18
  <!-- Custom CSS -->
19
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
20
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/animations.css') }}">
21
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
22
  {% block extra_head %}{% endblock %}
23
  </head>
24
  <body>
25
+ <header class="header glassmorphism">
26
  <div class="container">
27
+ <div class="header-content">
28
+ <h1 class="text-gradient">Mariam AI</h1>
29
+ </div>
30
  </div>
31
  </header>
32
 
 
34
  <div class="container">
35
  <ul class="nav nav-links">
36
  <li class="nav-item">
37
+ <a class="nav-link" href="https://mariam-241.vercel.app" title="Aller vers mariam-241" rel="noopener" aria-label="Aller vers mariam-241">
38
+ <!-- Home icon SVG -->
39
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
40
+ <path d="M3 9.5L12 3l9 6.5V20a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1V9.5z" />
41
+ </svg>
42
+ </a>
43
+ </li>
44
+ <li class="nav-item">
45
+ <a class="nav-link" href="{{ url_for('home') }}">Retour</a>
46
  </li>
47
  {% if request.endpoint and 'admin' in request.endpoint %}
48
  <li class="nav-item">
 
59
 
60
 
61
 
62
+ <!-- Footer removed per user request -->
63
+
64
  <!-- Bootstrap JS -->
65
  <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
66
+ <script src="{{ url_for('static', filename='js/theme.js') }}"></script>
67
  {% block extra_scripts %}{% endblock %}
68
  </body>
69
  </html>