@kennethsolomon/shipkit 3.2.0 → 3.4.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.
- package/README.md +9 -3
- package/commands/sk/help.md +2 -2
- package/package.json +1 -1
- package/skills/sk:e2e/SKILL.md +161 -10
- package/skills/sk:mvp/SKILL.md +266 -0
- package/skills/sk:mvp/references/design-system.md +136 -0
- package/skills/sk:mvp/references/landing-page.md +236 -0
- package/skills/sk:mvp/references/stacks/laravel.md +321 -0
- package/skills/sk:mvp/references/stacks/nextjs.md +189 -0
- package/skills/sk:mvp/references/stacks/nuxt.md +250 -0
- package/skills/sk:mvp/references/stacks/react-vite.md +287 -0
- package/skills/sk:review/SKILL.md +118 -11
- package/skills/sk:write-tests/SKILL.md +42 -3
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
# Landing Page — SaaS Section Patterns
|
|
2
|
+
|
|
3
|
+
Structure and patterns for the mandatory MVP landing page.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Required Sections (in order)
|
|
8
|
+
|
|
9
|
+
Every landing page must include ALL of these sections. Do not skip any.
|
|
10
|
+
|
|
11
|
+
### 1. Navbar
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
[Logo/Name] [Features] [Pricing] [Waitlist] [Join Waitlist →]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
- Sticky at top with backdrop blur.
|
|
18
|
+
- Logo or product name (text logo is fine — use display font).
|
|
19
|
+
- 2-4 nav links that scroll to sections (anchor links).
|
|
20
|
+
- CTA button on the right that scrolls to waitlist section.
|
|
21
|
+
- Mobile: collapse to hamburger.
|
|
22
|
+
|
|
23
|
+
### 2. Hero Section
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
┌─────────────────────────────────────────────────┐
|
|
27
|
+
│ │
|
|
28
|
+
│ [small eyebrow badge or label] │
|
|
29
|
+
│ │
|
|
30
|
+
│ Big Bold Headline That Sells │
|
|
31
|
+
│ the Benefit, Not the Feature │
|
|
32
|
+
│ │
|
|
33
|
+
│ A subheadline that elaborates in 1-2 │
|
|
34
|
+
│ sentences. Specific, not vague. │
|
|
35
|
+
│ │
|
|
36
|
+
│ [Primary CTA Button] [Secondary Link] │
|
|
37
|
+
│ │
|
|
38
|
+
│ ┌─────────────────────────────────┐ │
|
|
39
|
+
│ │ Hero visual / app preview / │ │
|
|
40
|
+
│ │ illustration / gradient box │ │
|
|
41
|
+
│ └─────────────────────────────────┘ │
|
|
42
|
+
│ │
|
|
43
|
+
└─────────────────────────────────────────────────┘
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
- **Headline**: Benefit-driven ("Ship faster" not "Project management tool"). 5-10 words max.
|
|
47
|
+
- **Subheadline**: Explain what it does and for whom. 1-2 sentences.
|
|
48
|
+
- **CTA**: Action verb + outcome ("Join the waitlist", "Get early access", "Start free").
|
|
49
|
+
- **Visual**: App screenshot mockup, abstract gradient, or SVG illustration. Never leave empty.
|
|
50
|
+
- **Eyebrow**: Optional small badge above headline ("Now in beta", "For developers", "AI-powered").
|
|
51
|
+
|
|
52
|
+
### 3. Social Proof Bar
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
Trusted by 500+ early adopters
|
|
56
|
+
[Logo] [Logo] [Logo] [Logo] [Logo]
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- Single line below hero.
|
|
60
|
+
- Use placeholder company names/logos (styled as gray text or simple SVG shapes).
|
|
61
|
+
- Alternatively: "Join 500+ people on the waitlist" with a count (fake but plausible).
|
|
62
|
+
- Keep subtle — muted colors, small text.
|
|
63
|
+
|
|
64
|
+
### 4. Features Grid
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
┌──────────┐ ┌──────────┐ ┌──────────┐
|
|
68
|
+
│ 🎯 Icon │ │ ⚡ Icon │ │ 🔒 Icon │
|
|
69
|
+
│ Title │ │ Title │ │ Title │
|
|
70
|
+
│ 2-line │ │ 2-line │ │ 2-line │
|
|
71
|
+
│ desc │ │ desc │ │ desc │
|
|
72
|
+
└──────────┘ └──────────┘ └──────────┘
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
- 3-6 features in a grid (3 columns desktop, 1 mobile).
|
|
76
|
+
- Each card: icon/emoji + title (3-5 words) + description (1-2 sentences).
|
|
77
|
+
- Icons: use emoji or simple SVG. Heroicons or Lucide if the stack supports it.
|
|
78
|
+
- Feature text must match the key features from Step 1.
|
|
79
|
+
|
|
80
|
+
### 5. How It Works
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
Step 1 Step 2 Step 3
|
|
84
|
+
① ② ③
|
|
85
|
+
Sign up Connect your See results
|
|
86
|
+
and set up data source in minutes
|
|
87
|
+
your account in one click
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
- 3 steps (rarely 4). Numbered or with icons.
|
|
91
|
+
- Each step: number/icon + title + 1-sentence description.
|
|
92
|
+
- Optional: connecting line or arrow between steps.
|
|
93
|
+
- Explains the user journey from signup to value.
|
|
94
|
+
|
|
95
|
+
### 6. Pricing
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
┌──────────┐ ┌──────────────┐ ┌──────────┐
|
|
99
|
+
│ Free │ │ Pro ⭐ │ │Enterprise│
|
|
100
|
+
│ $0/mo │ │ $29/mo │ │ Custom │
|
|
101
|
+
│ │ │ │ │ │
|
|
102
|
+
│ • 3 feat│ │ • All free │ │ • All │
|
|
103
|
+
│ • Basic │ │ • 5 more │ │ • Custom│
|
|
104
|
+
│ │ │ • Priority │ │ • SLA │
|
|
105
|
+
│ [Start] │ │ [Get Pro] │ │ [Contact]│
|
|
106
|
+
└──────────┘ └──────────────┘ └──────────┘
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
- 2-3 tiers. Middle tier highlighted (border, scale, badge).
|
|
110
|
+
- Prices should be fake but realistic for the product type.
|
|
111
|
+
- Each tier: name, price, feature list (5-7 items), CTA button.
|
|
112
|
+
- Free tier CTA → waitlist. Paid tier CTAs → waitlist (it's an MVP).
|
|
113
|
+
- All buttons route to the waitlist since nothing is real yet.
|
|
114
|
+
|
|
115
|
+
### 7. Testimonials
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
┌──────────────────────────────────┐
|
|
119
|
+
│ "This changed how I work..." │
|
|
120
|
+
│ │
|
|
121
|
+
│ [Avatar] Jane Smith │
|
|
122
|
+
│ CTO, TechCo │
|
|
123
|
+
└──────────────────────────────────┘
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
- 2-3 testimonial cards. Carousel or grid.
|
|
127
|
+
- Each: quote (1-3 sentences), name, role/company, avatar placeholder.
|
|
128
|
+
- Generate realistic-sounding quotes that align with the product's value prop.
|
|
129
|
+
- Avatars: use gradient circles with initials, or `ui-avatars.com` service.
|
|
130
|
+
- Mark clearly in code comments that these are placeholder testimonials.
|
|
131
|
+
|
|
132
|
+
### 8. Waitlist / CTA Section
|
|
133
|
+
|
|
134
|
+
```
|
|
135
|
+
┌─────────────────────────────────────────────────┐
|
|
136
|
+
│ │
|
|
137
|
+
│ Ready to try {Product}? │
|
|
138
|
+
│ Join the waitlist for early access. │
|
|
139
|
+
│ │
|
|
140
|
+
│ [email@example.com ] [Join →] │
|
|
141
|
+
│ │
|
|
142
|
+
│ ✓ No spam. We'll only email you when │
|
|
143
|
+
│ we launch. │
|
|
144
|
+
│ │
|
|
145
|
+
└─────────────────────────────────────────────────┘
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
- Prominent section near bottom (but before footer).
|
|
149
|
+
- Headline: compelling CTA ("Ready to X?", "Be the first to try").
|
|
150
|
+
- Email input + submit button on one line (desktop), stacked (mobile).
|
|
151
|
+
- Trust line below: "No spam" or "Join X others".
|
|
152
|
+
- States:
|
|
153
|
+
- **Default**: input + button enabled.
|
|
154
|
+
- **Loading**: button shows spinner, input disabled.
|
|
155
|
+
- **Success**: replace form with "You're on the list! We'll notify you at {email}."
|
|
156
|
+
- **Error**: show error message below input (invalid email, server error).
|
|
157
|
+
|
|
158
|
+
### 9. Footer
|
|
159
|
+
|
|
160
|
+
```
|
|
161
|
+
{Product Name} Features | Pricing | Waitlist
|
|
162
|
+
Built with ♥ © 2026 {Product}. All rights reserved.
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
- Simple. Product name, nav links (repeat from navbar), copyright.
|
|
166
|
+
- Optional: social links (use # placeholders).
|
|
167
|
+
- Dark or muted background to separate from content.
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Waitlist Backend Patterns
|
|
172
|
+
|
|
173
|
+
### Backend Stacks (Next.js, Nuxt, Laravel)
|
|
174
|
+
|
|
175
|
+
**API Route Pattern:**
|
|
176
|
+
|
|
177
|
+
```
|
|
178
|
+
POST /api/waitlist
|
|
179
|
+
Body: { "email": "user@example.com" }
|
|
180
|
+
|
|
181
|
+
→ Validate email format (regex or built-in validator)
|
|
182
|
+
→ Read waitlist.json from disk (create if doesn't exist)
|
|
183
|
+
→ Check for duplicate email
|
|
184
|
+
→ Append { email, timestamp, source: "landing-page" }
|
|
185
|
+
→ Write back to waitlist.json
|
|
186
|
+
→ Return { success: true, message: "You're on the list!" }
|
|
187
|
+
|
|
188
|
+
Errors:
|
|
189
|
+
→ Invalid email: 400 { success: false, message: "Please enter a valid email." }
|
|
190
|
+
→ Duplicate: 200 { success: true, message: "You're already on the list!" }
|
|
191
|
+
→ Server error: 500 { success: false, message: "Something went wrong. Try again." }
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
**waitlist.json format:**
|
|
195
|
+
```json
|
|
196
|
+
{
|
|
197
|
+
"entries": [
|
|
198
|
+
{
|
|
199
|
+
"email": "user@example.com",
|
|
200
|
+
"timestamp": "2026-03-18T10:30:00Z",
|
|
201
|
+
"source": "landing-page"
|
|
202
|
+
}
|
|
203
|
+
]
|
|
204
|
+
}
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The waitlist.json file should be in a non-public location:
|
|
208
|
+
- Next.js: project root `./waitlist.json` (outside `public/`)
|
|
209
|
+
- Nuxt: project root `./waitlist.json`
|
|
210
|
+
- Laravel: `storage/app/waitlist.json`
|
|
211
|
+
|
|
212
|
+
### Static Stacks (React + Vite)
|
|
213
|
+
|
|
214
|
+
**Formspree Pattern:**
|
|
215
|
+
|
|
216
|
+
```html
|
|
217
|
+
<form action="https://formspree.io/f/{your-form-id}" method="POST">
|
|
218
|
+
<input type="email" name="email" required />
|
|
219
|
+
<button type="submit">Join Waitlist</button>
|
|
220
|
+
</form>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
- Handle submission via JavaScript fetch for better UX (show loading/success states).
|
|
224
|
+
- Add a code comment: `// Replace {your-form-id} with your Formspree form ID — create one free at formspree.io`
|
|
225
|
+
- Handle Formspree's response format for success/error states.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Copywriting Guidelines
|
|
230
|
+
|
|
231
|
+
- **Headlines**: Lead with the benefit, not the feature. "Save 10 hours a week" > "Task management tool".
|
|
232
|
+
- **Subheadlines**: Be specific about who and what. "For freelancers who juggle too many clients" > "For everyone".
|
|
233
|
+
- **CTAs**: Action verb + outcome. "Get early access" > "Submit". "Join the waitlist" > "Sign up".
|
|
234
|
+
- **Feature descriptions**: Problem → solution format. "Stop losing track of invoices. Auto-track every payment in real time."
|
|
235
|
+
- **Tone**: Match the product. B2B SaaS = professional but warm. Dev tools = casual and direct. Consumer = friendly and energetic.
|
|
236
|
+
- **Never use**: "Revolutionize", "leverage", "synergy", "disrupt", "cutting-edge" (overused startup jargon).
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
# Laravel + Blade + Tailwind — Stack Reference
|
|
2
|
+
|
|
3
|
+
## Scaffold
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
composer create-project laravel/laravel {project-name}
|
|
7
|
+
cd {project-name}
|
|
8
|
+
npm install
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Laravel ships with Tailwind and Vite out of the box (Laravel 11+).
|
|
12
|
+
|
|
13
|
+
## Directory Structure
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
{project-name}/
|
|
17
|
+
├── resources/
|
|
18
|
+
│ ├── views/
|
|
19
|
+
│ │ ├── layouts/
|
|
20
|
+
│ │ │ ├── landing.blade.php ← landing page layout
|
|
21
|
+
│ │ │ └── app.blade.php ← app layout (sidebar)
|
|
22
|
+
│ │ ├── components/
|
|
23
|
+
│ │ │ ├── landing/
|
|
24
|
+
│ │ │ │ ├── navbar.blade.php
|
|
25
|
+
│ │ │ │ ├── hero.blade.php
|
|
26
|
+
│ │ │ │ ├── features.blade.php
|
|
27
|
+
│ │ │ │ ├── how-it-works.blade.php
|
|
28
|
+
│ │ │ │ ├── pricing.blade.php
|
|
29
|
+
│ │ │ │ ├── testimonials.blade.php
|
|
30
|
+
│ │ │ │ ├── waitlist-form.blade.php
|
|
31
|
+
│ │ │ │ └── footer.blade.php
|
|
32
|
+
│ │ │ ├── app/
|
|
33
|
+
│ │ │ │ ├── sidebar.blade.php
|
|
34
|
+
│ │ │ │ └── {feature components}
|
|
35
|
+
│ │ │ └── ui/
|
|
36
|
+
│ │ │ ├── button.blade.php
|
|
37
|
+
│ │ │ ├── input.blade.php
|
|
38
|
+
│ │ │ ├── card.blade.php
|
|
39
|
+
│ │ │ └── modal.blade.php
|
|
40
|
+
│ │ ├── landing.blade.php ← landing page
|
|
41
|
+
│ │ ├── dashboard.blade.php ← dashboard
|
|
42
|
+
│ │ ├── {feature-1}.blade.php
|
|
43
|
+
│ │ ├── {feature-2}.blade.php
|
|
44
|
+
│ │ └── settings.blade.php
|
|
45
|
+
│ ├── css/
|
|
46
|
+
│ │ └── app.css ← Tailwind directives + custom vars
|
|
47
|
+
│ └── js/
|
|
48
|
+
│ └── app.js ← Vite entry + Alpine.js for interactivity
|
|
49
|
+
├── routes/
|
|
50
|
+
│ └── web.php ← all routes
|
|
51
|
+
├── app/
|
|
52
|
+
│ └── Http/
|
|
53
|
+
│ └── Controllers/
|
|
54
|
+
│ └── WaitlistController.php
|
|
55
|
+
├── storage/
|
|
56
|
+
│ └── app/
|
|
57
|
+
│ └── waitlist.json ← email storage (auto-created)
|
|
58
|
+
├── public/
|
|
59
|
+
│ └── {compiled assets}
|
|
60
|
+
├── tailwind.config.js
|
|
61
|
+
├── vite.config.js
|
|
62
|
+
└── package.json
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Routes
|
|
66
|
+
|
|
67
|
+
`routes/web.php`:
|
|
68
|
+
|
|
69
|
+
```php
|
|
70
|
+
use App\Http\Controllers\WaitlistController;
|
|
71
|
+
|
|
72
|
+
// Landing page
|
|
73
|
+
Route::view('/', 'landing');
|
|
74
|
+
|
|
75
|
+
// App pages
|
|
76
|
+
Route::view('/dashboard', 'dashboard');
|
|
77
|
+
Route::view('/{feature-1}', '{feature-1}');
|
|
78
|
+
Route::view('/{feature-2}', '{feature-2}');
|
|
79
|
+
Route::view('/settings', 'settings');
|
|
80
|
+
|
|
81
|
+
// Waitlist API
|
|
82
|
+
Route::post('/api/waitlist', [WaitlistController::class, 'store']);
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Layouts
|
|
86
|
+
|
|
87
|
+
### Landing Layout (`resources/views/layouts/landing.blade.php`)
|
|
88
|
+
|
|
89
|
+
```blade
|
|
90
|
+
<!DOCTYPE html>
|
|
91
|
+
<html lang="en">
|
|
92
|
+
<head>
|
|
93
|
+
<meta charset="UTF-8">
|
|
94
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
95
|
+
<title>{{ $title ?? '{Product Name}' }}</title>
|
|
96
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
97
|
+
<link href="https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap" rel="stylesheet">
|
|
98
|
+
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
99
|
+
</head>
|
|
100
|
+
<body class="bg-bg text-fg font-body antialiased">
|
|
101
|
+
{{ $slot }}
|
|
102
|
+
</body>
|
|
103
|
+
</html>
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### App Layout (`resources/views/layouts/app.blade.php`)
|
|
107
|
+
|
|
108
|
+
```blade
|
|
109
|
+
<!DOCTYPE html>
|
|
110
|
+
<html lang="en">
|
|
111
|
+
<head>
|
|
112
|
+
<meta charset="UTF-8">
|
|
113
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
114
|
+
<title>{{ $title ?? '{Product Name}' }}</title>
|
|
115
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
116
|
+
<link href="https://fonts.googleapis.com/css2?family={DisplayFont}:wght@400;600;700;800&family={BodyFont}:wght@400;500;600&display=swap" rel="stylesheet">
|
|
117
|
+
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
|
118
|
+
</head>
|
|
119
|
+
<body class="bg-bg text-fg font-body antialiased">
|
|
120
|
+
<div class="flex min-h-screen">
|
|
121
|
+
<x-app.sidebar />
|
|
122
|
+
<main class="flex-1 p-6 lg:p-8">
|
|
123
|
+
{{ $slot }}
|
|
124
|
+
</main>
|
|
125
|
+
</div>
|
|
126
|
+
</body>
|
|
127
|
+
</html>
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
## Tailwind Config
|
|
131
|
+
|
|
132
|
+
`tailwind.config.js`:
|
|
133
|
+
|
|
134
|
+
```js
|
|
135
|
+
export default {
|
|
136
|
+
content: [
|
|
137
|
+
'./resources/**/*.blade.php',
|
|
138
|
+
'./resources/**/*.js',
|
|
139
|
+
],
|
|
140
|
+
theme: {
|
|
141
|
+
extend: {
|
|
142
|
+
colors: {
|
|
143
|
+
bg: 'var(--color-bg)',
|
|
144
|
+
fg: 'var(--color-fg)',
|
|
145
|
+
accent: 'var(--color-accent)',
|
|
146
|
+
muted: 'var(--color-muted)',
|
|
147
|
+
},
|
|
148
|
+
fontFamily: {
|
|
149
|
+
display: ['{DisplayFont}', 'serif'],
|
|
150
|
+
body: ['{BodyFont}', 'sans-serif'],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
CSS variables in `resources/css/app.css`:
|
|
158
|
+
|
|
159
|
+
```css
|
|
160
|
+
@tailwind base;
|
|
161
|
+
@tailwind components;
|
|
162
|
+
@tailwind utilities;
|
|
163
|
+
|
|
164
|
+
:root {
|
|
165
|
+
--color-bg: #xxxxxx;
|
|
166
|
+
--color-fg: #xxxxxx;
|
|
167
|
+
--color-accent: #xxxxxx;
|
|
168
|
+
--color-muted: #xxxxxx;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Interactivity with Alpine.js
|
|
173
|
+
|
|
174
|
+
Install Alpine.js for lightweight interactivity (modals, toasts, form states):
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
npm install alpinejs
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`resources/js/app.js`:
|
|
181
|
+
|
|
182
|
+
```js
|
|
183
|
+
import Alpine from 'alpinejs'
|
|
184
|
+
window.Alpine = Alpine
|
|
185
|
+
Alpine.start()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Waitlist Controller
|
|
189
|
+
|
|
190
|
+
`app/Http/Controllers/WaitlistController.php`:
|
|
191
|
+
|
|
192
|
+
```php
|
|
193
|
+
<?php
|
|
194
|
+
|
|
195
|
+
namespace App\Http\Controllers;
|
|
196
|
+
|
|
197
|
+
use Illuminate\Http\Request;
|
|
198
|
+
use Illuminate\Support\Facades\Storage;
|
|
199
|
+
|
|
200
|
+
class WaitlistController extends Controller
|
|
201
|
+
{
|
|
202
|
+
public function store(Request $request)
|
|
203
|
+
{
|
|
204
|
+
$request->validate([
|
|
205
|
+
'email' => 'required|email',
|
|
206
|
+
]);
|
|
207
|
+
|
|
208
|
+
$email = $request->input('email');
|
|
209
|
+
$path = 'waitlist.json';
|
|
210
|
+
|
|
211
|
+
// Read or create
|
|
212
|
+
$data = Storage::exists($path)
|
|
213
|
+
? json_decode(Storage::get($path), true)
|
|
214
|
+
: ['entries' => []];
|
|
215
|
+
|
|
216
|
+
// Check duplicate
|
|
217
|
+
$exists = collect($data['entries'])->contains('email', $email);
|
|
218
|
+
if ($exists) {
|
|
219
|
+
return response()->json(['success' => true, 'message' => "You're already on the list!"]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Append
|
|
223
|
+
$data['entries'][] = [
|
|
224
|
+
'email' => $email,
|
|
225
|
+
'timestamp' => now()->toISOString(),
|
|
226
|
+
'source' => 'landing-page',
|
|
227
|
+
];
|
|
228
|
+
|
|
229
|
+
Storage::put($path, json_encode($data, JSON_PRETTY_PRINT));
|
|
230
|
+
|
|
231
|
+
return response()->json(['success' => true, 'message' => "You're on the list!"]);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## Blade Component Patterns
|
|
237
|
+
|
|
238
|
+
### Anonymous Components (preferred for UI)
|
|
239
|
+
|
|
240
|
+
`resources/views/components/ui/button.blade.php`:
|
|
241
|
+
|
|
242
|
+
```blade
|
|
243
|
+
@props(['variant' => 'primary', 'size' => 'md'])
|
|
244
|
+
|
|
245
|
+
@php
|
|
246
|
+
$classes = match($variant) {
|
|
247
|
+
'primary' => 'bg-accent text-white hover:opacity-90',
|
|
248
|
+
'secondary' => 'border border-accent text-accent hover:bg-accent/10',
|
|
249
|
+
'ghost' => 'text-fg hover:bg-muted/20',
|
|
250
|
+
};
|
|
251
|
+
$sizes = match($size) {
|
|
252
|
+
'sm' => 'px-4 py-2 text-sm',
|
|
253
|
+
'md' => 'px-6 py-3 text-base',
|
|
254
|
+
'lg' => 'px-8 py-4 text-lg',
|
|
255
|
+
};
|
|
256
|
+
@endphp
|
|
257
|
+
|
|
258
|
+
<button {{ $attributes->merge(['class' => "$classes $sizes rounded-xl font-medium transition-all duration-200"]) }}>
|
|
259
|
+
{{ $slot }}
|
|
260
|
+
</button>
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Usage: `<x-ui.button variant="primary">Join Waitlist</x-ui.button>`
|
|
264
|
+
|
|
265
|
+
### Landing Page
|
|
266
|
+
|
|
267
|
+
`resources/views/landing.blade.php`:
|
|
268
|
+
|
|
269
|
+
```blade
|
|
270
|
+
<x-layouts.landing>
|
|
271
|
+
<x-landing.navbar />
|
|
272
|
+
<x-landing.hero />
|
|
273
|
+
<x-landing.features />
|
|
274
|
+
<x-landing.how-it-works />
|
|
275
|
+
<x-landing.pricing />
|
|
276
|
+
<x-landing.testimonials />
|
|
277
|
+
<x-landing.waitlist-form />
|
|
278
|
+
<x-landing.footer />
|
|
279
|
+
</x-layouts.landing>
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Waitlist Form with Alpine.js
|
|
283
|
+
|
|
284
|
+
```blade
|
|
285
|
+
<div x-data="{ email: '', status: 'idle', message: '' }">
|
|
286
|
+
<form @submit.prevent="
|
|
287
|
+
status = 'loading';
|
|
288
|
+
fetch('/api/waitlist', {
|
|
289
|
+
method: 'POST',
|
|
290
|
+
headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': '{{ csrf_token() }}' },
|
|
291
|
+
body: JSON.stringify({ email })
|
|
292
|
+
})
|
|
293
|
+
.then(r => r.json())
|
|
294
|
+
.then(d => { status = 'success'; message = d.message; })
|
|
295
|
+
.catch(() => { status = 'error'; message = 'Something went wrong.'; })
|
|
296
|
+
">
|
|
297
|
+
<template x-if="status !== 'success'">
|
|
298
|
+
<div class="flex gap-3">
|
|
299
|
+
<input x-model="email" type="email" placeholder="you@example.com" required
|
|
300
|
+
class="flex-1 px-4 py-3 rounded-lg border focus:ring-2 focus:ring-accent" />
|
|
301
|
+
<x-ui.button type="submit" x-bind:disabled="status === 'loading'">
|
|
302
|
+
<span x-show="status !== 'loading'">Join Waitlist</span>
|
|
303
|
+
<span x-show="status === 'loading'">Joining...</span>
|
|
304
|
+
</x-ui.button>
|
|
305
|
+
</div>
|
|
306
|
+
</template>
|
|
307
|
+
<p x-show="status === 'success'" x-text="message" class="text-green-600 font-medium"></p>
|
|
308
|
+
<p x-show="status === 'error'" x-text="message" class="text-red-500 text-sm mt-2"></p>
|
|
309
|
+
</form>
|
|
310
|
+
</div>
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
## Dev Server
|
|
314
|
+
|
|
315
|
+
```bash
|
|
316
|
+
# Run both in separate terminals (or use Concurrently)
|
|
317
|
+
php artisan serve # http://localhost:8000
|
|
318
|
+
npm run dev # Vite dev server for assets
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
Or use Laravel Herd if available (auto-serves at `{project-name}.test`).
|