@refai.code/vite-boost 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/vite-boost +1313 -0
  2. package/package.json +16 -0
package/bin/vite-boost ADDED
@@ -0,0 +1,1313 @@
1
+ #!/usr/bin/env zsh
2
+ emulate -L zsh
3
+ set -u
4
+
5
+ # ==========================================
6
+ # 🚀 Vite Boost: Refai.Code Final Ultimate
7
+ # ==========================================
8
+ # Features:
9
+ # - Smart Router (Scroll/Hash/Auth)
10
+ # - Glassmorphism UI (Login/Signup with Socials)
11
+ # - Real JSON Server Auth (Users save to db.json)
12
+ # - Concurrent Run (Vite + JSON Server)
13
+
14
+ echo "\n🚀 \033[1;36mVite Boost: Refai.Code Final Edition\033[0m\n"
15
+
16
+ # --- Validation: Check if it's a Vite project ---
17
+ if [ ! -f "package.json" ]; then
18
+ echo "❌ Error: package.json not found. Are you in the right directory?"
19
+ return 1
20
+ fi
21
+
22
+ # Check if Vite is in dependencies or devDependencies
23
+ if ! grep -q '"vite"' package.json; then
24
+ echo "⚠️ Warning: Vite not found in package.json."
25
+ if read -q "choice?This doesn't look like a Vite project. Continue anyway? (y/N) "; then
26
+ echo ""
27
+ else
28
+ echo "\n❌ Setup cancelled."
29
+ return 1
30
+ fi
31
+ fi
32
+
33
+ echo "✅ Detected Vite project. Proceeding...\n"
34
+
35
+ # --- 1. Interactive Prompts ---
36
+ ask() {
37
+ if read -q "choice?📦 Install $1? (y/N) "; then
38
+ echo "\n ✅ Selected $1"
39
+ eval "$2=true"
40
+ else
41
+ echo "\n ❌ Skipped $1"
42
+ eval "$2=false"
43
+ fi
44
+ echo ""
45
+ }
46
+
47
+ # Helper: Check file overwrite
48
+ check_overwrite() {
49
+ local file=$1
50
+ if [ -f "$file" ]; then
51
+ if read -q "choice?⚠️ $file already exists. Overwrite? (y/N) "; then
52
+ echo ""
53
+ return 0 # Yes, overwrite
54
+ else
55
+ echo "\n ⏭️ Skipped $file"
56
+ return 1 # No, skip
57
+ fi
58
+ fi
59
+ return 0 # File doesn't exist, proceed
60
+ }
61
+
62
+ # Defaults
63
+ tail=false
64
+ expr=false
65
+ pwa=false
66
+ axio=false
67
+ json=false
68
+
69
+ # Track what was installed
70
+ installed_packages=()
71
+
72
+ ask "TailwindCSS" tail
73
+ ask "Axios (HTTP Client)" axio
74
+ ask "PWA Support" pwa
75
+ ask "JSON Server (Real Database & Auth)" json
76
+
77
+ # ==========================================
78
+ # 🛠️ Architecture Setup
79
+ # ==========================================
80
+ echo "\n📁 Creating project structure..."
81
+ mkdir -p src/{components,pages,router,styles,services,utils,assets}
82
+
83
+ # -----------------------------
84
+ # 1. Utilities
85
+ # -----------------------------
86
+ echo "\n🔧 Generating utility files..."
87
+ if check_overwrite "src/utils/helpers.js"; then
88
+ cat > src/utils/helpers.js <<'EOF'
89
+ export const sanitize = (str) => {
90
+ if (!str) return '';
91
+ const div = document.createElement('div');
92
+ div.textContent = str;
93
+ return div.innerHTML;
94
+ };
95
+
96
+ export const renderStars = (rating) => {
97
+ const stars = Math.round(rating);
98
+ let html = '';
99
+ for (let i = 1; i <= 5; i++) {
100
+ html += `<span style="color: ${i <= stars ? '#f59e0b' : '#d1d5db'}">★</span>`;
101
+ }
102
+ return html;
103
+ };
104
+
105
+ export const sleep = (ms) => new Promise(r => setTimeout(r, ms));
106
+ EOF
107
+ fi
108
+
109
+ cat > src/utils/toast.js <<'EOF'
110
+ export const toast = {
111
+ show: (msg, type = 'error') => {
112
+ const el = document.createElement('div');
113
+ const color = type === 'success' ? '#10b981' : '#ef4444';
114
+ el.style.cssText = `
115
+ position: fixed; top: 20px; right: 20px;
116
+ background: ${color}; color: white; padding: 1rem 1.5rem;
117
+ border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15);
118
+ z-index: 9999; animation: slideIn 0.3s ease-out; font-weight: 500;
119
+ `;
120
+ el.textContent = msg;
121
+ document.body.appendChild(el);
122
+ setTimeout(() => {
123
+ el.style.opacity = '0';
124
+ el.style.transform = 'translateY(-20px)';
125
+ el.style.transition = 'all 0.3s';
126
+ setTimeout(() => el.remove(), 300);
127
+ }, 3000);
128
+ }
129
+ };
130
+ EOF
131
+
132
+ # -----------------------------
133
+ # 2. API & Auth Services
134
+ # -----------------------------
135
+ # Dynamic API Service
136
+ if [ "$axio" = true ]; then
137
+ cat > src/services/api.js <<'EOF'
138
+ import axios from 'axios';
139
+ import { toast } from '../utils/toast.js';
140
+
141
+ const API_URL = 'http://localhost:3000';
142
+
143
+ const handle = async (promise) => {
144
+ try {
145
+ const res = await promise;
146
+ return res.data;
147
+ } catch (err) {
148
+ console.error(err);
149
+ toast.show(err.response?.data?.message || err.message || 'API Error', 'error');
150
+ throw err;
151
+ }
152
+ };
153
+
154
+ export const api = {
155
+ get: (url) => handle(axios.get(`${API_URL}${url}`)),
156
+ post: (url, data) => handle(axios.post(`${API_URL}${url}`, data)),
157
+ put: (url, data) => handle(axios.put(`${API_URL}${url}`, data)),
158
+ delete: (url) => handle(axios.delete(`${API_URL}${url}`))
159
+ };
160
+ EOF
161
+ else
162
+ cat > src/services/api.js <<'EOF'
163
+ import { toast } from '../utils/toast.js';
164
+
165
+ const API_URL = 'http://localhost:3000';
166
+
167
+ const request = async (url, options = {}) => {
168
+ try {
169
+ const res = await fetch(`${API_URL}${url}`, {
170
+ headers: { 'Content-Type': 'application/json' },
171
+ ...options,
172
+ });
173
+ if (!res.ok) throw new Error(`API Error: ${res.statusText}`);
174
+ return await res.json();
175
+ } catch (err) {
176
+ console.error(err);
177
+ toast.show(err.message, 'error');
178
+ throw err;
179
+ }
180
+ };
181
+
182
+ export const api = {
183
+ get: (url) => request(url),
184
+ post: (url, data) => request(url, { method: 'POST', body: JSON.stringify(data) }),
185
+ delete: (url) => request(url, { method: 'DELETE' }),
186
+ put: (url, data) => request(url, { method: 'PUT', body: JSON.stringify(data) })
187
+ };
188
+ EOF
189
+ fi
190
+
191
+ # Auth Service with persistent state
192
+ cat > src/services/auth.js <<'EOF'
193
+
194
+ import { api } from './api.js';
195
+ import { toast } from '../utils/toast.js';
196
+
197
+ export const authState = {
198
+ user: JSON.parse(localStorage.getItem('user')) || null,
199
+ listeners: [],
200
+
201
+ subscribe(fn) { this.listeners.push(fn); },
202
+ notify() { this.listeners.forEach(fn => fn(this.user)); },
203
+
204
+ async login(email, password) {
205
+ try {
206
+ // For JSON Server, we simulate login by fetching users
207
+ const users = await api.get(`/users?email=${email}`);
208
+ const user = users.find(u => u.password === password);
209
+
210
+ if (!user) {
211
+ throw new Error('Invalid credentials');
212
+ }
213
+
214
+ this.user = user;
215
+ localStorage.setItem('user', JSON.stringify(user));
216
+ this.notify();
217
+ toast.show(`Welcome back, ${user.firstName}!`, 'success');
218
+ return true;
219
+ } catch (err) {
220
+ toast.show('Login failed: Invalid email or password');
221
+ return false;
222
+ }
223
+ },
224
+
225
+ async signup(data) {
226
+ try {
227
+ // Check if user exists
228
+ const existing = await api.get(`/users?email=${data.email}`);
229
+ if (existing.length > 0) throw new Error('Email already registered');
230
+
231
+ const newUser = {
232
+ ...data,
233
+ avatar: `https://ui-avatars.com/api/?name=${data.firstName}+${data.lastName}&background=4f46e5&color=fff`
234
+ };
235
+
236
+ await api.post('/users', newUser);
237
+ toast.show('Account created! Please login.', 'success');
238
+ return true;
239
+ } catch (err) {
240
+ toast.show(err.message);
241
+ return false;
242
+ }
243
+ },
244
+
245
+ // Edit profile method
246
+ async updateProfile(updatedData) {
247
+ try {
248
+ if (!this.user) throw new Error('No user logged in');
249
+
250
+ const updatedUser = { ...this.user, ...updatedData };
251
+ await api.put(`/users/${this.user.id}`, updatedUser);
252
+
253
+ this.user = updatedUser;
254
+ localStorage.setItem('user', JSON.stringify(updatedUser));
255
+ this.notify();
256
+ toast.show('Profile updated successfully', 'success');
257
+ return true;
258
+ } catch (err) {
259
+ toast.show('Profile update failed: ' + err.message);
260
+ return false;
261
+ }
262
+ },
263
+
264
+ logout() {
265
+ this.user = null;
266
+ localStorage.removeItem('user');
267
+ this.notify();
268
+ toast.show('Logged out successfully', 'success');
269
+ window.location.href = '/';
270
+ }
271
+ };
272
+ EOF
273
+
274
+ # -----------------------------
275
+ # 3. Router
276
+ # -----------------------------
277
+ cat > src/router/LiteRouter.js <<'EOF'
278
+ export class LiteRouter {
279
+ constructor(routes, appElement) {
280
+ this.routes = routes;
281
+ this.app = document.querySelector(appElement);
282
+
283
+ window.addEventListener('popstate', () => this.loadRoute());
284
+
285
+ document.body.addEventListener('click', e => {
286
+ const anchor = e.target.closest('a[href^="#"]');
287
+ if (anchor) {
288
+ e.preventDefault();
289
+ const id = anchor.getAttribute('href');
290
+ if (window.location.pathname !== '/') {
291
+ this.navigateTo('/' + id);
292
+ } else {
293
+ this.scrollToId(id);
294
+ }
295
+ return;
296
+ }
297
+
298
+ const link = e.target.closest('[data-link]');
299
+ if (link) {
300
+ e.preventDefault();
301
+ this.navigateTo(link.getAttribute('href'));
302
+ }
303
+ });
304
+
305
+ // Initial Load Logic
306
+ window.addEventListener('load', () => {
307
+ this.loadRoute();
308
+ });
309
+ }
310
+
311
+ navigateTo(url) {
312
+ history.pushState(null, null, url);
313
+ this.loadRoute();
314
+ }
315
+
316
+ async loadRoute() {
317
+ let path = window.location.pathname;
318
+
319
+ // Check hash intent from other pages
320
+ let hashTarget = '';
321
+ if (path.includes('/#')) {
322
+ const parts = path.split('/#');
323
+ path = '/';
324
+ hashTarget = '#' + parts[1];
325
+ }
326
+
327
+ if (path === '/index.html') path = '/';
328
+
329
+ const match = this.routes.find(r => {
330
+ const rx = new RegExp("^" + r.path.replace(/:(\w+)/g, "(?<$1>[^/]+)") + "$");
331
+ return path.match(rx);
332
+ }) || this.routes.find(r => r.path === '*');
333
+
334
+ const result = path.match(new RegExp("^" + match.path.replace(/:(\w+)/g, "(?<$1>[^/]+)") + "$"));
335
+ const params = result?.groups || {};
336
+ const search = new URLSearchParams(window.location.search);
337
+
338
+ // Layout
339
+ if (match.layout) {
340
+ if (!this.currentLayout || this.currentLayout !== match.layout) {
341
+ this.app.innerHTML = await match.layout();
342
+ this.currentLayout = match.layout;
343
+ }
344
+ }
345
+
346
+ const outlet = this.app.querySelector('[data-outlet]') || this.app;
347
+
348
+ // Spinner
349
+ outlet.innerHTML = '<div class="spinner-container"><div class="spinner"></div></div>';
350
+
351
+ try {
352
+ const view = await match.component({ params, query: search });
353
+ outlet.innerHTML = view;
354
+ } catch (err) {
355
+ outlet.innerHTML = `<div class="error-page"><h2>Error</h2><p>${err.message}</p></div>`;
356
+ }
357
+
358
+ if (match.title) document.title = match.title;
359
+
360
+ // Scroll Handling
361
+ if (hashTarget) {
362
+ setTimeout(() => this.scrollToId(hashTarget), 300);
363
+ } else if (!window.location.hash) {
364
+ window.scrollTo({ top: 0, behavior: 'smooth' });
365
+ }
366
+ }
367
+
368
+ scrollToId(id) {
369
+ const el = document.querySelector(id);
370
+ if (el) el.scrollIntoView({ behavior: 'smooth', block: 'start' });
371
+ }
372
+
373
+ init() {
374
+ // defer init call to window.load often safest or immediate
375
+ }
376
+ }
377
+ EOF
378
+
379
+ # -----------------------------
380
+ # 4. Styles (Glass, Spinner, Cards)
381
+ # -----------------------------
382
+ cat > src/styles/main.css <<'EOF'
383
+ :root {
384
+ --primary: #4f46e5;
385
+ --bg: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
386
+ --text: #1f2937;
387
+ --glass: rgba(255, 255, 255, 0.85);
388
+ --glass-border: rgba(255, 255, 255, 0.4);
389
+ }
390
+
391
+ * { box-sizing: border-box; }
392
+ html { scroll-behavior: smooth; }
393
+ body { margin:0; font-family: 'Outfit', sans-serif; background: var(--bg); min-height: 100vh; color: var(--text); }
394
+ html, body { overflow-x: hidden; }
395
+
396
+ /* Keyframes */
397
+ @keyframes spin { to { transform: rotate(360deg); } }
398
+ @keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
399
+ @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
400
+
401
+ /* Utils */
402
+ .container { max-width: 1200px; margin: 0 auto; padding: 2rem; }
403
+ .spinner-container { display: flex; justify-content: center; padding: 4rem; width: 100%; }
404
+ .spinner { width: 40px; height: 40px; border: 4px solid #ddd; border-top-color: var(--primary); border-radius: 50%; animation: spin 0.8s linear infinite; }
405
+
406
+ /* Buttons */
407
+ .btn {
408
+ padding: 0.75rem 1.5rem; border-radius: 8px; border: none; font-weight: 600; cursor: pointer;
409
+ transition: all 0.2s; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 0.5rem;
410
+ }
411
+ .btn-primary {
412
+ background: var(--primary);
413
+ color: white;
414
+ box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.3);
415
+ }
416
+ .btn-primary:hover {
417
+ transform: translateY(-2px);
418
+ box-shadow: 0 6px 15px -3px rgba(79, 70, 229, 0.4);
419
+ }
420
+ .btn-social {
421
+ background: white;
422
+ border: 1px solid #e5e7eb; color: #374151;
423
+ width: 100%; font-weight: 500;}
424
+ .btn-social:hover {
425
+ background: #f9fafb;
426
+ border-color: #d1d5db;
427
+ }
428
+
429
+ /* Glass Card */
430
+ .glass-panel {
431
+ background: var(--glass);
432
+ backdrop-filter: blur(12px);
433
+ border: 1px solid var(--glass-border);
434
+ border-radius: 16px;
435
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.05);
436
+ padding: 2.5rem;
437
+ }
438
+
439
+ /* Auth Specific */
440
+ .auth-container {
441
+ display: flex;
442
+ justify-content: center;
443
+ align-items: center;
444
+ min-height: 80vh;
445
+ }
446
+ .auth-box {
447
+ width: 100%; max-width: 420px;
448
+ }
449
+ input {
450
+ width: 100%;
451
+ padding: 0.8rem 1rem;
452
+ margin-bottom: 1rem;
453
+ border: 1px solid #d1d5db;
454
+ border-radius: 8px;
455
+ font-size: 1rem;
456
+ transition: border 0.2s;
457
+ }
458
+ input:focus {
459
+ outline: none;
460
+ border-color: var(--primary);
461
+ box-shadow: 0 0 0 2px rgba(79, 70, 229, 0.1); }
462
+
463
+ /* Navbar Mobile Responsive */
464
+ .navbar {
465
+ position: sticky;
466
+ top: 0;
467
+ z-index: 1000;
468
+ background: rgba(255,255,255,0.95);
469
+ backdrop-filter: blur(10px);
470
+ border-bottom: 1px solid rgba(0,0,0,0.05);
471
+ padding: 0.5rem 0;
472
+ }
473
+ .nav-inner {
474
+ max-width: 1200px;
475
+ margin:0 auto;
476
+ padding: 0 1.5rem;
477
+ display: flex;
478
+ justify-content: space-between;
479
+ align-items: center;
480
+ height: 60px;
481
+ }
482
+ .nav-links {
483
+ display: flex;
484
+ align-items: center;
485
+ gap: 2rem;
486
+ }
487
+ .nav-links a {
488
+ text-decoration: none;
489
+ color: var(--text);
490
+ font-weight: 500;
491
+ padding: 0.5rem 1rem;
492
+ border-radius: 8px;
493
+ transition: all 0.2s;
494
+ }
495
+ .nav-links a:hover {
496
+ color: var(--primary);
497
+ background: rgba(0,0,0,0.05);
498
+ }
499
+ .nav-links a.active {
500
+ color: white;
501
+ background: var(--primary);
502
+ }
503
+
504
+ /* Hamburger */
505
+ .hamburger {
506
+ display: none;
507
+ font-size: 1.5rem;
508
+ cursor: pointer;
509
+ background:none;
510
+ border:none; color:var(--text); }
511
+
512
+ /* Mobile Menu */
513
+ @media (max-width: 768px) {
514
+ .nav-links {
515
+ display: none;
516
+ position: absolute;
517
+ top: 60px;
518
+ left: 0;
519
+ width: 100%;
520
+ background: white;
521
+ flex-direction: column;
522
+ padding: 1rem 0;
523
+ box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1);
524
+ border-top: 1px solid #eee;
525
+ }
526
+ .nav-links.active {
527
+ display: flex;
528
+ }
529
+ .nav-links a {
530
+ margin: 0; padding: 1rem;
531
+ width: 100%; text-align: center;
532
+ }
533
+ .nav-links div {
534
+ border:none !important;
535
+ padding: 1rem 0 !important;
536
+ margin:0 !important;
537
+ width: 100%;
538
+ display: flex;
539
+ justify-content: center;
540
+ }
541
+ .hamburger {
542
+ display: block;
543
+ }
544
+ }
545
+
546
+ /* Modal Overlay System */
547
+ .modal-overlay {
548
+ position: fixed; inset: 0; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px);
549
+ display: flex; justify-content: center; align-items: center; z-index: 2000;
550
+ animation: fadeIn 0.2s ease-out;
551
+ }
552
+ .modal-content {
553
+ background: white; width: 100%; max-width: 450px; border-radius: 16px; padding: 2rem;
554
+ box-shadow: 0 20px 25px -5px rgba(0,0,0,0.1);
555
+ position: relative; animation: slideUp 0.3s cubic-bezier(0.16, 1, 0.3, 1);
556
+ }
557
+ .close-modal {
558
+ position: absolute; top: 1rem; right: 1rem; background: none; border: none; font-size: 1.5rem; cursor: pointer; color: #999;
559
+ }
560
+ @keyframes slideUp { from { transform: translateY(20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } }
561
+
562
+ .grid {
563
+ display: grid;
564
+ grid-template-columns: repeat(auto-fill,
565
+ minmax(280px, 1fr));
566
+ gap: 2rem;
567
+ }
568
+ .card {
569
+ background: white;
570
+ border-radius: 12px;
571
+ overflow: hidden;
572
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
573
+ transition: transform 0.2s;
574
+ display: flex;
575
+ flex-direction: column;
576
+ }
577
+ .card:hover {
578
+ transform: translateY(-5px);
579
+ box-shadow: 0 12px 20px -5px rgba(0,0,0,0.1); }
580
+ .card-img {
581
+ width: 100%;
582
+ height: 220px;
583
+ object-fit: cover;
584
+ }
585
+ .card-body {
586
+ padding: 1.5rem;
587
+ flex: 1; display:
588
+ flex; flex-direction: column;
589
+ }
590
+
591
+ /* Product Detail Page Responsive */
592
+ @media (max-width: 768px) {
593
+ .glass-panel {
594
+ grid-template-columns: 1fr !important;
595
+ padding: 1.5rem !important;
596
+ }
597
+ .product-detail-img {
598
+ margin-bottom: 1.5rem;
599
+ }
600
+ }
601
+ EOF
602
+
603
+ # -----------------------------
604
+ # 5. Components
605
+ # -----------------------------
606
+ cat > src/components/Navbar.js <<'EOF'
607
+ import { authState } from '../services/auth.js';
608
+ import { openLoginModal, openProfileModal, openSignupModal } from '../utils/modals.js';
609
+
610
+ export const Navbar = () => {
611
+ const user = authState.user;
612
+
613
+ // Toggle Mobile Menu Logic
614
+ setTimeout(() => {
615
+ const btn = document.getElementById('hamburger-btn');
616
+ const menu = document.getElementById('nav-menu');
617
+ if(btn && menu) {
618
+ btn.onclick = () => menu.classList.toggle('active');
619
+ // Close menu when clicking any link
620
+ menu.querySelectorAll('a, button').forEach(el => {
621
+ el.addEventListener('click', () => menu.classList.remove('active'));
622
+ });
623
+ }
624
+ }, 0);
625
+
626
+ return `<nav class="navbar">
627
+ <div class="nav-inner">
628
+ <a href="/" class="logo" style="font-weight:800; font-size:1.4rem; text-decoration:none; color:var(--text)" data-link>
629
+ Refai<span style="color:var(--primary)">.Code</span>
630
+ </a>
631
+
632
+ <button class="hamburger" id="hamburger-btn">☰</button>
633
+
634
+ <div class="nav-links" id="nav-menu">
635
+ <a href="/" data-link>Home</a>
636
+ <a href="#about">About</a>
637
+ <a href="/products" data-link>Products</a>
638
+
639
+ <div style="margin-left: 2rem; padding-left: 2rem; border-left: 1px solid #ddd;">
640
+ ${user ? `
641
+ <div style="display:flex; align-items:center; gap:0.6rem; cursor:pointer;" onclick="window.openProfile()">
642
+ <img src="${user.avatar}" style="width:34px; height:34px; border-radius:50%; border:2px solid var(--primary);" alt="Avatar">
643
+ <span style="font-weight:600; font-size:0.9rem;">${user.firstName}</span>
644
+ </div>
645
+ ` : `
646
+ <button class="btn btn-primary" onclick="window.openLogin()" style="padding:0.4rem 1.2rem; font-size:0.9rem;">Sign In</button>
647
+ `}
648
+ </div>
649
+ </div>
650
+ </div>
651
+ </nav>
652
+ `;
653
+ };
654
+
655
+ // Global handlers for Modals
656
+ window.openLogin = openLoginModal;
657
+ window.openProfile = openProfileModal;
658
+ window.openSignup = openSignupModal;
659
+ EOF
660
+
661
+ # --- Modal Manager (New File) ---
662
+ cat > src/utils/modals.js <<'EOF'
663
+ import { authState } from '../services/auth.js';
664
+ import { toast } from './toast.js';
665
+
666
+ const createOverlay = (content) => {
667
+ const existing = document.querySelector('.modal-overlay');
668
+ if(existing) existing.remove();
669
+
670
+ const overlay = document.createElement('div');
671
+ overlay.className = 'modal-overlay';
672
+ overlay.innerHTML = `
673
+ <div class="modal-content">
674
+ <button class="close-modal" onclick="this.closest('.modal-overlay').remove()">×</button>
675
+ ${content}
676
+ </div>
677
+ `;
678
+ overlay.addEventListener('click', (e) => {
679
+ if(e.target === overlay) overlay.remove();
680
+ });
681
+ document.body.appendChild(overlay);
682
+ return overlay;
683
+ };
684
+
685
+ export const openLoginModal = () => {
686
+ createOverlay(`
687
+ <h2 style="text-align:center; margin-bottom:1rem;">Welcome Back</h2>
688
+ <p style="text-align:center; color:#6b7280; margin-bottom:1.5rem; font-size:0.9rem;">Sign in to your account</p>
689
+
690
+ <form onsubmit="window.handleLogin(event)">
691
+ <input type="email" name="email" value="admin@refai.code" placeholder="Email Address" required>
692
+ <input type="password" name="password" value="admin123" placeholder="Password" required>
693
+ <button type="submit" class="btn btn-primary" style="width:100%; margin-bottom:1rem;">Sign In</button>
694
+ </form>
695
+
696
+ <div style="display:flex; align-items:center; margin:1.5rem 0; color:#9ca3af; font-size:0.85rem;">
697
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
698
+ <span style="padding:0 1rem;">OR CONTINUE WITH</span>
699
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
700
+ </div>
701
+
702
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:0.8rem; margin-bottom:1rem;">
703
+ <button class="btn btn-social" onclick="toast.show('Google Auth Coming Soon', 'error')">
704
+ <svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M21.35,11.1H12.18V13.83H18.69C18.36,17.64 15.19,19.27 12.19,19.27C8.36,19.27 5,16.25 5,12.61C5,9.5 7.81,5.7 12.19,5.7C13.63,5.7 15.71,6.5 16.55,7.34L19.07,4.8C16.8,2.77 14.28,2.15 12.18,2.15C5.83,2.15 1.15,7.39 1.15,12.61C1.15,17.97 4.96,22.85 12.18,22.85C18.27,22.85 22.85,17.7 22.85,12.61C22.85,11.75 22.75,11.53 22.65,11.1H21.35Z"/></svg> Google
705
+ </button>
706
+ <button class="btn btn-social" onclick="toast.show('GitHub Auth Coming Soon', 'error')">
707
+ <svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z"/></svg> GitHub
708
+ </button>
709
+ </div>
710
+
711
+ <p style="text-align:center; font-size:0.85rem; color:#6b7280;">
712
+ Don't have an account? <a href="#" onclick="event.preventDefault(); window.openSignup()" style="color:var(--primary); font-weight:600; text-decoration:none;">Sign Up</a>
713
+ </p>
714
+ `);
715
+ };
716
+
717
+ export const openSignupModal = () => {
718
+ createOverlay(`
719
+ <h2 style="text-align:center; margin-bottom:1rem;">Create Account</h2>
720
+ <p style="text-align:center; color:#6b7280; margin-bottom:1.5rem; font-size:0.9rem;">Join Refai.Code today</p>
721
+
722
+ <form onsubmit="window.handleSignup(event)">
723
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:0.5rem;">
724
+ <input type="text" name="firstName" placeholder="First Name" required>
725
+ <input type="text" name="lastName" placeholder="Last Name" required>
726
+ </div>
727
+ <input type="email" name="email" placeholder="Email Address" required>
728
+ <input type="password" name="password" placeholder="Create Password" required>
729
+ <button type="submit" class="btn btn-primary" style="width:100%; margin-bottom:1rem;">Create Account</button>
730
+ </form>
731
+
732
+ <div style="display:flex; align-items:center; margin:1.5rem 0; color:#9ca3af; font-size:0.85rem;">
733
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
734
+ <span style="padding:0 1rem;">OR SIGN UP WITH</span>
735
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
736
+ </div>
737
+
738
+ <div style="display:grid; grid-template-columns:1fr 1fr; gap:0.8rem; margin-bottom:1rem;">
739
+ <button class="btn btn-social" onclick="toast.show('Google Auth Coming Soon', 'error')">
740
+ <svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M21.35,11.1H12.18V13.83H18.69C18.36,17.64 15.19,19.27 12.19,19.27C8.36,19.27 5,16.25 5,12.61C5,9.5 7.81,5.7 12.19,5.7C13.63,5.7 15.71,6.5 16.55,7.34L19.07,4.8C16.8,2.77 14.28,2.15 12.18,2.15C5.83,2.15 1.15,7.39 1.15,12.61C1.15,17.97 4.96,22.85 12.18,22.85C18.27,22.85 22.85,17.7 22.85,12.61C22.85,11.75 22.75,11.53 22.65,11.1H21.35Z"/></svg> Google
741
+ </button>
742
+ <button class="btn btn-social" onclick="toast.show('GitHub Auth Coming Soon', 'error')">
743
+ <svg width="18" height="18" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z"/></svg> GitHub
744
+ </button>
745
+ </div>
746
+
747
+ <p style="text-align:center; font-size:0.85rem; color:#6b7280;">
748
+ Already have an account? <a href="#" onclick="event.preventDefault(); window.openLogin()" style="color:var(--primary); font-weight:600; text-decoration:none;">Log In</a>
749
+ </p>
750
+ `);
751
+ };
752
+
753
+ export const openProfileModal = () => {
754
+ const u = authState.user;
755
+ if(!u) return;
756
+
757
+ createOverlay(`
758
+ <div style="text-align:center; margin-bottom:1.5rem;">
759
+ <img src="${u.avatar}" style="width:80px; height:80px; border-radius:50%; margin-bottom:1rem; border:3px solid var(--primary);">
760
+ <h3>Edit Profile</h3>
761
+ </div>
762
+ <form onsubmit="window.handleUpdateProfile(event)">
763
+ <div style="display:flex; gap:0.5rem;">
764
+ <input type="text" name="firstName" value="${u.firstName}" placeholder="First Name">
765
+ <input type="text" name="lastName" value="${u.lastName}" placeholder="Last Name">
766
+ </div>
767
+ <input type="email" name="email" value="${u.email}" readonly style="background:#f3f4f6; color:#999;" title="Email cannot be changed">
768
+ <input type="password" name="password" placeholder="New Password (Optional)">
769
+
770
+ <div style="display:flex; gap:1rem; margin-top:1rem;">
771
+ <button type="button" class="btn" style="flex:1; background:#fee; color:red;" onclick="window.auth.logout()">Logout</button>
772
+ <button type="submit" class="btn btn-primary" style="flex:1;">Update Profile</button>
773
+ </div>
774
+ </form>
775
+ `);
776
+ };
777
+
778
+ // Logic Handlers attached to window
779
+ window.handleLogin = async (e) => {
780
+ e.preventDefault();
781
+ const email = e.target.email.value;
782
+ const pass = e.target.password.value;
783
+ const success = await authState.login(email, pass);
784
+ if(success) document.querySelector('.modal-overlay')?.remove();
785
+ };
786
+
787
+ window.handleSignup = async (e) => {
788
+ e.preventDefault();
789
+ const data = {
790
+ firstName: e.target.firstName.value,
791
+ lastName: e.target.lastName.value,
792
+ email: e.target.email.value,
793
+ password: e.target.password.value
794
+ };
795
+ const success = await authState.signup(data);
796
+ if(success) {
797
+ document.querySelector('.modal-overlay')?.remove();
798
+ setTimeout(() => openLoginModal(), 300);
799
+ }
800
+ };
801
+
802
+ window.handleUpdateProfile = async (e) => {
803
+ e.preventDefault();
804
+ const data = {
805
+ firstName: e.target.firstName.value,
806
+ lastName: e.target.lastName.value,
807
+ password: e.target.password.value
808
+ };
809
+ const updatedUser = { ...authState.user, ...data };
810
+ if(!data.password) delete updatedUser.password;
811
+ if(JSON.stringify(updatedUser) === JSON.stringify(authState.user))
812
+ toast.show('No changes made to update.', 'info');
813
+ else {
814
+ const success = await authState.updateProfile(updatedUser);
815
+ if(success) document.querySelector('.modal-overlay')?.remove();
816
+ }
817
+ };
818
+
819
+ // Expose Auth globally for logout
820
+ import { authState as auth } from '../services/auth.js';
821
+ window.auth = auth;
822
+ EOF
823
+
824
+ cat > src/components/Layout.js <<'EOF'
825
+ import { Navbar } from './Navbar.js';
826
+
827
+ export const Layout = () =>
828
+ `
829
+ <div id="nav-mount">${Navbar()}</div>
830
+ <main>
831
+ <div data-outlet></div>
832
+ </main>
833
+ <footer style="background: white; border-top: 1px solid #eaeaea; padding: 4rem 0; margin-top: 4rem;">
834
+ <div class="container" style="text-align:center;">
835
+ <h3 style="margin-bottom:1rem; opacity:0.8;">Refai.Code</h3>
836
+ <div style="display:flex; justify-content:center; gap:1.5rem; margin-bottom:2rem;">
837
+ <a href="#" style="color:#666; text-decoration:none;">Instagram</a>
838
+ <a href="#" style="color:#666; text-decoration:none;">Twitter</a>
839
+ <a href="#" style="color:#666; text-decoration:none;">GitHub</a>
840
+ </div>
841
+ <p style="color:#999; font-size:0.9rem;">© 2026. All rights reserved.</p>
842
+ </div>
843
+ </footer>
844
+ `;
845
+ EOF
846
+
847
+ # -----------------------------
848
+ # 6. Pages (Auth & Logic)
849
+ # -----------------------------
850
+ cat > src/pages/LoginPage.js <<'EOF'
851
+ import { authState } from '../services/auth.js';
852
+
853
+ export const LoginPage = () => {
854
+ setTimeout(() => {
855
+ const form = document.getElementById('loginForm');
856
+ if(form) {
857
+ form.onsubmit = async (e) => {
858
+ e.preventDefault();
859
+ const email = form.email.value;
860
+ const pass = form.password.value;
861
+ const success = await authState.login(email, pass);
862
+ if(success) window.location.href = '/';
863
+ };
864
+ }
865
+ }, 0);
866
+
867
+ return `
868
+ <div class="auth-container">
869
+ <div class="glass-panel auth-box">
870
+ <div style="text-align:center; margin-bottom:2rem;">
871
+ <h2 style="font-size:2rem; margin-bottom:0.5rem;">Welcome Back</h2>
872
+ <p style="color:#6b7280;">Enter your credentials to access your account.</p>
873
+ </div>
874
+
875
+ <form id="loginForm">
876
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">Email Address</label>
877
+ <input type="email" name="email" value="admin@refai.code" required placeholder="name@example.com">
878
+
879
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">Password</label>
880
+ <input type="password" name="password" value="admin123" required placeholder="••••••••">
881
+
882
+ <button type="submit" class="btn btn-primary" style="width:100%; padding:0.9rem;">Sign In</button>
883
+ </form>
884
+
885
+ <div style="display:flex; align-items:center; margin: 2rem 0; color:#9ca3af; font-size:0.9rem;">
886
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
887
+ <span style="padding:0 1rem;">OR CONTINUE WITH</span>
888
+ <div style="flex:1; height:1px; background:#e5e7eb;"></div>
889
+ </div>
890
+
891
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:1rem;">
892
+ <button class="btn btn-social">
893
+ <svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M21.35,11.1H12.18V13.83H18.69C18.36,17.64 15.19,19.27 12.19,19.27C8.36,19.27 5,16.25 5,12.61C5,9.5 7.81,5.7 12.19,5.7C13.63,5.7 15.71,6.5 16.55,7.34L19.07,4.8C16.8,2.77 14.28,2.15 12.18,2.15C5.83,2.15 1.15,7.39 1.15,12.61C1.15,17.97 4.96,22.85 12.18,22.85C18.27,22.85 22.85,17.7 22.85,12.61C22.85,11.75 22.75,11.53 22.65,11.1H21.35Z"/></svg> Google
894
+ </button>
895
+ <button class="btn btn-social">
896
+ <svg width="20" height="20" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,0 2,12C2,16.42 4.87,20.17 8.84,21.5C9.34,21.58 9.5,21.27 9.5,21C9.5,20.77 9.5,20.14 9.5,19.31C6.73,19.91 6.14,17.97 6.14,17.97C5.68,16.81 5.03,16.5 5.03,16.5C4.12,15.88 5.1,15.9 5.1,15.9C6.1,15.97 6.63,16.93 6.63,16.93C7.5,18.45 8.97,18 9.54,17.76C9.63,17.11 9.89,16.67 10.17,16.42C7.95,16.17 5.62,15.31 5.62,11.5C5.62,10.39 6,9.5 6.65,8.79C6.55,8.54 6.2,7.5 6.75,6.15C6.75,6.15 7.59,5.88 9.5,7.17C10.29,6.95 11.15,6.84 12,6.84C12.85,6.84 13.71,6.95 14.5,7.17C16.41,5.88 17.25,6.15 17.25,6.15C17.8,7.5 17.45,8.54 17.35,8.79C18,9.5 18.38,10.39 18.38,11.5C18.38,15.32 16.04,16.16 13.81,16.41C14.17,16.72 14.5,17.33 14.5,18.26C14.5,19.6 14.5,20.68 14.5,21C14.5,21.27 14.66,21.59 15.17,21.5C19.14,20.16 22,16.42 22,12A10,10 0 0,0 12,2Z"/></svg> GitHub
897
+ </button>
898
+ </div>
899
+
900
+ <p style="text-align:center; margin-top:2rem; font-size:0.9rem;">
901
+ Don't have an account? <a href="/signup" data-link style="color:var(--primary); font-weight:600; text-decoration:none;">Sign Up</a>
902
+ </p>
903
+ </div>
904
+ </div>
905
+ `;
906
+ };
907
+ EOF
908
+
909
+ cat > src/pages/SignupPage.js <<'EOF'
910
+ import { authState } from '../services/auth.js';
911
+
912
+ export const SignupPage = () => {
913
+ setTimeout(() => {
914
+ const form = document.getElementById('signupForm');
915
+ if(form) {
916
+ form.onsubmit = async (e) => {
917
+ e.preventDefault();
918
+ const data = {
919
+ firstName: form.firstName.value,
920
+ lastName: form.lastName.value,
921
+ email: form.email.value,
922
+ password: form.password.value
923
+ };
924
+ const success = await authState.signup(data);
925
+ if(success) window.location.href = '/login';
926
+ };
927
+ }
928
+ }, 0);
929
+
930
+ return `
931
+ <div class="auth-container">
932
+ <div class="glass-panel auth-box">
933
+ <div style="text-align:center; margin-bottom:2rem;">
934
+ <h2 style="font-size:2rem; margin-bottom:0.5rem;">Join Us</h2>
935
+ <p style="color:#6b7280;">Create your account to get started.</p>
936
+ </div>
937
+
938
+ <form id="signupForm">
939
+ <div style="display:grid; grid-template-columns: 1fr 1fr; gap:1rem;">
940
+ <div>
941
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">First Name</label>
942
+ <input type="text" name="firstName" required placeholder="John">
943
+ </div>
944
+ <div>
945
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">Last Name</label>
946
+ <input type="text" name="lastName" required placeholder="Doe">
947
+ </div>
948
+ </div>
949
+
950
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">Email Address</label>
951
+ <input type="email" name="email" required placeholder="name@example.com">
952
+
953
+ <label style="font-weight:600; font-size:0.9rem; margin-bottom:0.5rem; display:block;">Password</label>
954
+ <input type="password" name="password" required placeholder="Create a password">
955
+
956
+ <button type="submit" class="btn btn-primary" style="width:100%; padding:0.9rem;">Create Account</button>
957
+ </form>
958
+
959
+ <p style="text-align:center; margin-top:2rem; font-size:0.9rem;">
960
+ Already have an account? <a href="/login" data-link style="color:var(--primary); font-weight:600; text-decoration:none;">Log In</a>
961
+ </p>
962
+ </div>
963
+ </div>
964
+ `;
965
+ };
966
+ EOF
967
+
968
+ # Standard Pages (Home, Products, Details)
969
+ cat > src/pages/HomePage.js <<'EOF'
970
+ import { api } from '../services/api.js';
971
+
972
+ export const HomePage = async () => {
973
+ return `
974
+ <header style="padding: 8rem 2rem; text-align:center; background: radial-gradient(circle at center, rgba(79,70,229,0.1), transparent 70%);">
975
+ <div class="container">
976
+ <span style="background:rgba(79,70,229,0.1); color:var(--primary); padding:0.4rem 1rem; border-radius:30px; font-weight:600; font-size:0.9rem;">
977
+ v2.0 Ultimate
978
+ </span>
979
+ <h1 style="font-size:4rem; margin:1.5rem 0; line-height:1.1;">
980
+ Build with <span style="background: linear-gradient(to right, #4f46e5, #ec4899); -webkit-background-clip: text; -webkit-text-fill-color: transparent;">Precision</span>.
981
+ </h1>
982
+ <p style="font-size:1.4rem; color:#6b7280; max-width:600px; margin:0 auto 2.5rem;">
983
+ A fully functional MVC architecture with Authentication, Routing, and Live Database integration.
984
+ </p>
985
+ <div style="display:flex; gap:1rem; justify-content:center;">
986
+ <a href="/products" class="btn btn-primary" style="padding:1rem 2rem; font-size:1.1rem;" data-link>Fetch Data</a>
987
+ <a href="https://github.com/refai" target="_blank" class="btn btn-social" style="width:auto; padding:1rem 2rem; font-size:1.1rem;">Documentation</a>
988
+ </div>
989
+ </div>
990
+ </header>
991
+
992
+ <section id="about" style="background:white; padding:6rem 2rem;">
993
+ <div class="container">
994
+ <div style="text-align:center; margin-bottom:4rem;">
995
+ <h2 style="font-size:2.5rem; margin-bottom:1rem;">Why Refai.Code?</h2>
996
+ <p style="color:#6b7280;">Built for modern developers who need speed and structure.</p>
997
+ </div>
998
+
999
+ <div class="grid">
1000
+ <div class="glass-panel" style="text-align:center; border:none; background:#f9fafb;">
1001
+ <div style="font-size:2.5rem; margin-bottom:1rem;">⚡</div>
1002
+ <h3 style="margin-bottom:0.5rem;">Concurrent Run</h3>
1003
+ <p style="color:#6b7280;">Vite & Server run in a single command.</p>
1004
+ </div>
1005
+ <div class="glass-panel" style="text-align:center; border:none; background:#f9fafb;">
1006
+ <div style="font-size:2.5rem; margin-bottom:1rem;">🔐</div>
1007
+ <h3 style="margin-bottom:0.5rem;">Real Auth</h3>
1008
+ <p style="color:#6b7280;">Users saved to JSON DB with persistent login.</p>
1009
+ </div>
1010
+ <div class="glass-panel" style="text-align:center; border:none; background:#f9fafb;">
1011
+ <div style="font-size:2.5rem; margin-bottom:1rem;">💅</div>
1012
+ <h3 style="margin-bottom:0.5rem;">Glass UI</h3>
1013
+ <p style="color:#6b7280;">Modern aesthetics with blurred backgrounds.</p>
1014
+ </div>
1015
+ </div>
1016
+ </div>
1017
+ </section>
1018
+ `;
1019
+ };
1020
+ EOF
1021
+
1022
+ cat > src/pages/ProductsPage.js <<'EOF'
1023
+ import { api } from '../services/api.js';
1024
+ import { renderStars } from '../utils/helpers.js';
1025
+
1026
+ export const ProductsPage = async () => {
1027
+ let products = [];
1028
+ try {
1029
+ products = await api.get('/products');
1030
+ } catch (e) {
1031
+ // Fallback
1032
+ }
1033
+
1034
+ return `<div class="container" style="padding-top:4rem;">
1035
+ <h1 style="margin-bottom:2rem;">Featured Products</h1>
1036
+ <div class="grid">
1037
+ ${products.map(p => `
1038
+ <div class="card">
1039
+ <img src="${p.image}" class="card-img" alt="${p.name}">
1040
+ <div class="card-body">
1041
+ <div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.5rem;">
1042
+ <h3 style="font-size:1.1rem; margin:0; font-weight:700;">${p.name}</h3>
1043
+ <span style="font-size:0.9rem; font-weight:600; color:var(--primary);">$${p.price}</span>
1044
+ </div>
1045
+ <div style="margin-bottom:1rem; font-size:0.9rem; color:#f59e0b;">
1046
+ ${renderStars(p.rating)}
1047
+ </div>
1048
+ <p style="font-size:0.9rem; color:#6b7280; flex:1; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical; margin-bottom:1.5rem;">
1049
+ ${p.description}
1050
+ </p>
1051
+ <a href="/products/${p.id}" class="btn btn-primary" style="width:100%; justify-content:center;" data-link>View Details</a>
1052
+ </div>
1053
+ </div>
1054
+ `).join('')}
1055
+ </div>
1056
+ </div>`;
1057
+ };
1058
+ EOF
1059
+
1060
+ cat > src/pages/ProductDetailsPage.js <<'EOF'
1061
+ import { api } from '../services/api.js';
1062
+ import { renderStars } from '../utils/helpers.js';
1063
+
1064
+ export const ProductDetailsPage = async ({ params }) => {
1065
+ try {
1066
+ const p = await api.get(`/products/${params.id}`);
1067
+ return `<div class="container" style="padding-top:4rem;">
1068
+ <a href="/products" data-link style="display:inline-flex; align-items:center; color:#6b7280; text-decoration:none; margin-bottom:2rem; font-weight:500;">
1069
+ <svg style="width:20px; margin-right:5px;" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
1070
+ Back to Products
1071
+ </a>
1072
+
1073
+ <div class="glass-panel" style="display:grid; grid-template-columns: 1fr 1fr; gap:3rem; padding:3rem;">
1074
+ <img src="${p.image}" class="product-detail-img" style="width:100%; border-radius:12px;" alt="${p.name}">
1075
+ <div>
1076
+ <h1 style="font-size:2.5rem; margin:0 0 1rem 0;">${p.name}</h1>
1077
+ <div style="display:flex; align-items:center; gap:1rem; margin-bottom:2rem;">
1078
+ <span style="font-size:1.5rem; font-weight:700; color:var(--primary);">$${p.price}</span>
1079
+ <div style="border-left:1px solid #ddd; padding-left:1rem; font-size:1.1rem; color:#f59e0b;">
1080
+ ${renderStars(p.rating)}
1081
+ </div>
1082
+ </div>
1083
+ <p style="color:#4b5563; line-height:1.7; font-size:1.1rem; margin-bottom:3rem;">
1084
+ ${p.description}
1085
+ </p>
1086
+ <button class="btn btn-primary" style="padding:1rem 2rem; font-size:1.1rem; width:100%;" onclick="alert('Added to cart!')">
1087
+ Add to Cart
1088
+ </button>
1089
+ </div>
1090
+ </div>`;
1091
+ } catch (e) {
1092
+ return `<div class="container"><h1>Product Not Found</h1></div>`;
1093
+ }
1094
+ };
1095
+ EOF
1096
+
1097
+ # -----------------------------
1098
+ # 7. Main Entry Point
1099
+ # -----------------------------
1100
+ # Smart handling: Append to existing main.js instead of replacing
1101
+ if [ -f "src/main.js" ]; then
1102
+ echo "\n⚠️ src/main.js already exists."
1103
+ if read -q "choice?Do you want to append router code to it? (y/N) "; then
1104
+ echo "\n✅ Appending router logic to existing main.js..."
1105
+
1106
+ # Backup first
1107
+ cp src/main.js src/main.js.backup
1108
+ echo "📦 Backup created: src/main.js.backup"
1109
+
1110
+ # Append router imports and setup
1111
+ cat >> src/main.js <<'EOF'
1112
+
1113
+ // ===== Refai.Code Router Setup (Auto-generated) =====
1114
+ import { LiteRouter } from './router/LiteRouter.js';
1115
+ import { Layout } from './components/Layout.js';
1116
+ import { Navbar } from './components/Navbar.js';
1117
+ import { authState } from './services/auth.js';
1118
+
1119
+ import { HomePage } from './pages/HomePage.js';
1120
+ import { ProductsPage } from './pages/ProductsPage.js';
1121
+ import { ProductDetailsPage } from './pages/ProductDetailsPage.js';
1122
+ import { LoginPage } from './pages/LoginPage.js';
1123
+ import { SignupPage } from './pages/SignupPage.js';
1124
+
1125
+ const routes = [
1126
+ { path: '/', component: HomePage, layout: Layout, title: 'Home | Refai.Code' },
1127
+ { path: '/products', component: ProductsPage, layout: Layout, title: 'Products' },
1128
+ { path: '/products/:id', component: ProductDetailsPage, layout: Layout, title: 'Details' },
1129
+ { path: '/login', component: LoginPage, layout: Layout, title: 'Login' },
1130
+ { path: '/logout', component: () => { authState.logout(); return '<div></div>'; } },
1131
+ { path: '/signup', component: SignupPage, layout: Layout, title: 'Signup' },
1132
+ { path: '*', component: () => '<h1 class="container">404</h1>', layout: Layout }
1133
+ ];
1134
+
1135
+ authState.subscribe(() => {
1136
+ const el = document.getElementById('nav-mount');
1137
+ if (el) el.innerHTML = Navbar();
1138
+ });
1139
+
1140
+ const app = new LiteRouter(routes, '#app');
1141
+ app.init();
1142
+ EOF
1143
+ else
1144
+ echo "\n⏭️ Skipped modifying main.js"
1145
+ fi
1146
+ else
1147
+ # File doesn't exist, create new one
1148
+ cat > src/main.js <<'EOF'
1149
+ import './styles/main.css';
1150
+ import { LiteRouter } from './router/LiteRouter.js';
1151
+ import { Layout } from './components/Layout.js';
1152
+ import { Navbar } from './components/Navbar.js';
1153
+ import { authState } from './services/auth.js';
1154
+
1155
+ import { HomePage } from './pages/HomePage.js';
1156
+ import { ProductsPage } from './pages/ProductsPage.js';
1157
+ import { ProductDetailsPage } from './pages/ProductDetailsPage.js';
1158
+ import { LoginPage } from './pages/LoginPage.js';
1159
+ import { SignupPage } from './pages/SignupPage.js';
1160
+
1161
+ const routes = [
1162
+ { path: '/', component: HomePage, layout: Layout, title: 'Home | Refai.Code' },
1163
+ { path: '/products', component: ProductsPage, layout: Layout, title: 'Products' },
1164
+ { path: '/products/:id', component: ProductDetailsPage, layout: Layout, title: 'Details' },
1165
+ { path: '/login', component: LoginPage, layout: Layout, title: 'Login' },
1166
+ { path: '/logout', component: () => { authState.logout(); return '<div></div>'; } },
1167
+ { path: '/signup', component: SignupPage, layout: Layout, title: 'Signup' },
1168
+ { path: '*', component: () => '<h1 class="container">404</h1>', layout: Layout }
1169
+ ];
1170
+
1171
+ // Subscribe Navbar to Auth Changes
1172
+ authState.subscribe(() => {
1173
+ const el = document.getElementById('nav-mount');
1174
+ if (el) el.innerHTML = Navbar();
1175
+ });
1176
+
1177
+ const app = new LiteRouter(routes, '#app');
1178
+ app.init();
1179
+ EOF
1180
+ fi
1181
+
1182
+ # -----------------------------
1183
+ # 8. Dependency & Concurrent
1184
+ # -----------------------------
1185
+ # Create DB.json
1186
+ cat > db.json <<'EOF'
1187
+ {
1188
+ "users": [
1189
+ {
1190
+ "id": 1,
1191
+ "email": "admin@refai.code",
1192
+ "password": "admin123",
1193
+ "firstName": "Super",
1194
+ "lastName": "Admin",
1195
+ "avatar": "https://ui-avatars.com/api/?name=Refai+Code&background=4f46e5&color=fff"
1196
+ }
1197
+ ],
1198
+ "products": [
1199
+ {
1200
+ "id": 1,
1201
+ "name": "Sony WH-1000XM5",
1202
+ "price": 348,
1203
+ "rating": 4.8,
1204
+ "image": "https://images.unsplash.com/photo-1618366712010-f4ae9c647dcb?auto=format&fit=crop&w=600&q=80",
1205
+ "description": "Industry-leading noise canceling with two processors controlling 8 microphones for unprecedented noise cancellation."
1206
+ },
1207
+ {
1208
+ "id": 2,
1209
+ "name": "Apple MacBook Air M2",
1210
+ "price": 1099,
1211
+ "rating": 4.9,
1212
+ "image": "https://images.unsplash.com/photo-1517336714731-489689fd1ca4?auto=format&fit=crop&w=600&q=80",
1213
+ "description": "Redesigned around the next-generation M2 chip. Strikingly thin and brings exceptional speed and power efficiency."
1214
+ }
1215
+ ]
1216
+ }
1217
+ EOF
1218
+
1219
+ # -----------------------------
1220
+ # 8. Package Installation
1221
+ # -----------------------------
1222
+ echo "\n📦 Installing Dependencies..."
1223
+ if [ -f "pnpm-lock.yaml" ]; then PM="pnpm"; elif [ -f "yarn.lock" ]; then PM="yarn"; else PM="npm"; fi
1224
+
1225
+ $PM install
1226
+ installed_packages+=("Core dependencies")
1227
+
1228
+ if [ "$tail" = true ]; then
1229
+ echo "🎨 Setting up TailwindCSS..."
1230
+ $PM install -D tailwindcss postcss autoprefixer
1231
+
1232
+ # Initialize tailwind config
1233
+ if [ "$PM" = "pnpm" ]; then
1234
+ pnpm exec tailwindcss init -p
1235
+ elif [ "$PM" = "yarn" ]; then
1236
+ yarn tailwindcss init -p
1237
+ else
1238
+ npx -y tailwindcss init -p
1239
+ fi
1240
+
1241
+ cat > src/styles/tailwind.css <<EOF
1242
+ @tailwind base;
1243
+ @tailwind components;
1244
+ @tailwind utilities;
1245
+ EOF
1246
+ echo "@import './tailwind.css';" | cat - src/styles/main.css > temp && mv temp src/styles/main.css
1247
+ installed_packages+=("TailwindCSS")
1248
+ fi
1249
+
1250
+ if [ "$axio" = true ]; then
1251
+ echo "📡 Installing Axios..."
1252
+ $PM install axios
1253
+ installed_packages+=("Axios")
1254
+ fi
1255
+
1256
+ if [ "$json" = true ]; then
1257
+ echo "🚀 Setting up JSON Server..."
1258
+ $PM install -D json-server concurrently
1259
+
1260
+ # Inject Scripts into package.json (dev for Vite only, dev:full for both)
1261
+ node -e "const fs=require('fs');const p=JSON.parse(fs.readFileSync('package.json','utf8'));p.scripts=p.scripts||{};p.scripts['dev:full']='concurrently \"vite\" \"json-server db.json --port 3000\"';if(!p.scripts.dev || p.scripts.dev.includes('concurrently')) p.scripts.dev='vite';fs.writeFileSync('package.json',JSON.stringify(p,null,2));"
1262
+ installed_packages+=("JSON Server" "Concurrently")
1263
+ fi
1264
+
1265
+ # PWA Setup
1266
+ if [ "$pwa" = true ]; then
1267
+ echo "📱 Setting up PWA..."
1268
+ $PM install -D vite-plugin-pwa
1269
+ installed_packages+=("Vite PWA Plugin")
1270
+ cat > vite.config.js <<EOF
1271
+ import { defineConfig } from 'vite';
1272
+ import path from 'path';
1273
+ import { VitePWA } from 'vite-plugin-pwa';
1274
+
1275
+ export default defineConfig({
1276
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
1277
+ plugins: [
1278
+ VitePWA({ registerType: 'autoUpdate' }),
1279
+ ]
1280
+ });
1281
+ EOF
1282
+ else
1283
+ cat > vite.config.js <<EOF
1284
+ import { defineConfig } from 'vite';
1285
+ import path from 'path';
1286
+
1287
+ export default defineConfig({
1288
+ resolve: { alias: { '@': path.resolve(__dirname, './src') } },
1289
+ });
1290
+ EOF
1291
+ fi
1292
+
1293
+ # -----------------------------
1294
+ # Final Summary
1295
+ # -----------------------------
1296
+ echo "\n✅ \033[1;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m"
1297
+ echo "\n🎉 \033[1;32mSetup Complete!\033[0m Created by Refai.Code\n"
1298
+
1299
+ if [ ${#installed_packages[@]} -gt 0 ]; then
1300
+ echo "📦 \033[1;36mInstalled Packages:\033[0m"
1301
+ for pkg in "${installed_packages[@]}"; do
1302
+ echo " ✓ $pkg"
1303
+ done
1304
+ echo ""
1305
+ fi
1306
+
1307
+ echo "🚀 \033[1;33mNext Steps:\033[0m"
1308
+ echo " 1. Run \033[1m$PM run dev\033[0m to start Vite dev server"
1309
+ if [ "$json" = true ]; then
1310
+ echo " 2. Run \033[1m$PM run dev:full\033[0m to start Vite + JSON Server together"
1311
+ echo " 3. Or run \033[1mnpx json-server db.json --port 3000\033[0m in a separate terminal"
1312
+ fi
1313
+ echo "\n✅ \033[1;32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\033[0m\n"
package/package.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "name": "@refai.code/vite-boost",
3
+ "version": "0.1.0",
4
+ "description": "Vite Vanilla JS booster script (optional Tailwind/Axios/PWA/JSON-server)",
5
+ "license": "MIT",
6
+ "private": false,
7
+ "bin": {
8
+ "vite-boost": "bin/vite-boost"
9
+ },
10
+ "files": [
11
+ "bin"
12
+ ],
13
+ "engines": {
14
+ "node": ">=18"
15
+ }
16
+ }