create-spine 1.0.2 → 1.0.4
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/final-test/AGENTS.md +5 -0
- package/final-test/CLAUDE.md +1 -0
- package/final-test/README.md +37 -0
- package/final-test/app/Refund/page.tsx +340 -0
- package/final-test/app/api/auth/[...nextauth]/route.ts +6 -0
- package/final-test/app/api/generate/route.ts +57 -0
- package/final-test/app/dashboard/page.tsx +713 -0
- package/final-test/app/faq/page.tsx +343 -0
- package/final-test/app/globals.css +130 -0
- package/final-test/app/layout.tsx +33 -0
- package/final-test/app/page.tsx +650 -0
- package/final-test/app/pricing/page.tsx +23 -0
- package/final-test/app/privacy/page.tsx +328 -0
- package/final-test/app/terms/page.tsx +12 -0
- package/final-test/components/ui/badge.tsx +52 -0
- package/final-test/components/ui/button.tsx +58 -0
- package/final-test/components/ui/card.tsx +103 -0
- package/final-test/components/ui/separator.tsx +25 -0
- package/final-test/components/ui/textarea.tsx +18 -0
- package/final-test/components/ui/ui/footer/page.tsx +103 -0
- package/final-test/components/ui/ui/navbar/navbar.tsx +0 -0
- package/final-test/components.json +25 -0
- package/final-test/desktop.ini +6 -0
- package/final-test/eslint.config.mjs +18 -0
- package/final-test/lib/auth.ts +19 -0
- package/final-test/lib/prisma.ts +7 -0
- package/final-test/lib/utils.ts +6 -0
- package/final-test/next.config.ts +7 -0
- package/final-test/package-lock.json +9794 -0
- package/final-test/package.json +35 -0
- package/final-test/postcss.config.mjs +7 -0
- package/final-test/public/X.svg +1 -0
- package/final-test/public/github.svg +2 -0
- package/final-test/public/google.svg +2 -0
- package/final-test/src/components/ui/badge.tsx +58 -0
- package/final-test/src/components/ui/button.tsx +90 -0
- package/final-test/src/components/ui/card.tsx +103 -0
- package/final-test/src/components/ui/textarea.tsx +29 -0
- package/final-test/src/lib/utils.ts +6 -0
- package/final-test/tsconfig.json +34 -0
- package/index.js +3 -6
- package/package.json +1 -1
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Spine — FAQ page
|
|
5
|
+
* Reuses the exact nav / footer / GithubIcon / CTA-card markup from the
|
|
6
|
+
* landing page so this drops in as a real route (e.g. app/faq/page.tsx)
|
|
7
|
+
* without looking like a separate site.
|
|
8
|
+
*
|
|
9
|
+
* To wire this up for real:
|
|
10
|
+
* - Swap "mailto:support@spine.io" for your real support address
|
|
11
|
+
* - Point the GitHub link at your actual repo
|
|
12
|
+
* - FAQ_ITEMS below is plain content — edit freely, add/remove items
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { useState, useMemo } from "react"
|
|
16
|
+
import {
|
|
17
|
+
Search, ChevronDown, Route, Terminal, Database, Zap, CreditCard,
|
|
18
|
+
FileCode, ShieldCheck, Copy, Check,
|
|
19
|
+
} from "lucide-react"
|
|
20
|
+
import { Button } from "@/components/ui/button"
|
|
21
|
+
import { Card, CardContent } from "@/components/ui/card"
|
|
22
|
+
import { Input } from "@/components/ui/input"
|
|
23
|
+
|
|
24
|
+
const cx = (...c) => c.filter(Boolean).join(" ")
|
|
25
|
+
|
|
26
|
+
const GithubIcon = ({ className }) => (
|
|
27
|
+
<svg viewBox="0 0 24 24" fill="currentColor" className={className}>
|
|
28
|
+
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
|
|
29
|
+
</svg>
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
/* ════════════════════════════════════════════════════════════════
|
|
33
|
+
CONTENT
|
|
34
|
+
════════════════════════════════════════════════════════════════ */
|
|
35
|
+
const FAQ_CATEGORIES = [
|
|
36
|
+
{ id: "getting-started", label: "Getting Started", icon: Route },
|
|
37
|
+
{ id: "cli", label: "CLI & Installation", icon: Terminal },
|
|
38
|
+
{ id: "stacks", label: "Stacks & Templates", icon: Database },
|
|
39
|
+
{ id: "usage", label: "Usage & Limits", icon: Zap },
|
|
40
|
+
{ id: "billing", label: "Pricing & Billing", icon: CreditCard },
|
|
41
|
+
{ id: "code", label: "Generated Code", icon: FileCode },
|
|
42
|
+
{ id: "privacy", label: "Privacy & Security", icon: ShieldCheck },
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
const FAQ_ITEMS = [
|
|
46
|
+
{ id: 1, category: "getting-started", q: "What is Spine?", a: "Spine turns a plain-English description of your backend into a downloadable, production-ready scaffold. Instead of picking a stack and wiring up boilerplate by hand, you describe what you're building — Spine generates the schema, routes, and config to match." },
|
|
47
|
+
{ id: 2, category: "getting-started", q: "How do I generate my first scaffold?", a: "Head to the homepage, describe your backend in the prompt box, choose a baseline stack, and hit generate. No account needed — you'll get a ZIP with the full project in seconds." },
|
|
48
|
+
{ id: 3, category: "getting-started", q: "Do I need to create an account?", a: "No. Free generations are fully anonymous. An account only matters if you want to save templates, keep usage history, or unlock Pro features like unlimited generations." },
|
|
49
|
+
{ id: 4, category: "cli", q: "How do I install the CLI?", a: "Run this in your terminal — nothing to install globally. It walks you through the same prompt-and-stack flow as the website, then scaffolds the project straight into a local folder.", hasChip: true },
|
|
50
|
+
{ id: 5, category: "cli", q: "Does the CLI need any setup, like Node or a config file?", a: "Just Node.js and npx, which most environments already have. There's no config file to write first — the CLI asks for your prompt and stack choice interactively." },
|
|
51
|
+
{ id: 6, category: "stacks", q: "Which stacks does Spine support?", a: "Four baseline stacks: Next.js Fullstack, MERN, a standalone Microservice, and a Hono Edge API. Your description gets adapted to whichever one you pick, so the same prompt produces idiomatic code for that stack." },
|
|
52
|
+
{ id: 7, category: "stacks", q: "What's actually inside a generated scaffold?", a: "Typically a Prisma schema modeled from the entities in your prompt, REST API routes, auth scaffolding, Stripe webhook handlers if you mention payments, a README explaining the structure, and a starter .env file." },
|
|
53
|
+
{ id: 8, category: "stacks", q: "Can I request a new stack or template?", a: "Spine is in open beta (v0.1.0) and the template list is actively growing. Open an issue on GitHub with what you'd like to see — beta feedback directly shapes what gets added next." },
|
|
54
|
+
{ id: 9, category: "usage", q: "How many generations do I get for free?", a: "5 AI generations a day, no account required. The counter resets daily at midnight UTC; Pro removes the daily cap entirely." },
|
|
55
|
+
{ id: 10, category: "usage", q: "What if a generation doesn't come out the way I expected?", a: "Refine your prompt and generate again — being specific about entities, relationships, and auth requirements up front usually gets you closer on the first try. Each attempt does count toward your daily limit." },
|
|
56
|
+
{ id: 11, category: "billing", q: "What's the difference between Free and Pro?", a: "Free covers 5 generations a day across all 4 stacks, plus the CLI — genuinely usable long-term. Pro is $9/month and adds unlimited generations, private saved templates, custom stack presets, and team sharing." },
|
|
57
|
+
{ id: 12, category: "billing", q: "Can I cancel Pro anytime?", a: "Yes — Pro is a monthly subscription you can cancel anytime from your account settings. You'll keep Pro access through the end of the billing period you've already paid for." },
|
|
58
|
+
{ id: 13, category: "code", q: "Can I customize the code after downloading?", a: "Completely. Everything Spine generates is plain source you own outright — no hidden runtime dependency on Spine, no lock-in. Edit, extend, or delete anything you don't need." },
|
|
59
|
+
{ id: 14, category: "code", q: "Is the generated code production-ready?", a: "It's built to be a strong, working starting point — real routes, real schema, real auth — but treat it like any scaffold: review it, add your own tests, and adapt it to your team's conventions before shipping." },
|
|
60
|
+
{ id: 15, category: "privacy", q: "Do you store my prompts or generated code?", a: "Anonymous free generations aren't tied to an account. If you sign in and save a template, that's stored to your account so you can reuse it later — you can delete any saved template anytime from your dashboard." },
|
|
61
|
+
{ id: 16, category: "privacy", q: "Is Spine open source?", a: "The CLI and core generator are part of the open beta — check the GitHub repo for the current license and source, and to follow what's changing." },
|
|
62
|
+
]
|
|
63
|
+
|
|
64
|
+
/* ════════════════════════════════════════════════════════════════
|
|
65
|
+
SMALL PIECES
|
|
66
|
+
════════════════════════════════════════════════════════════════ */
|
|
67
|
+
function CliChip() {
|
|
68
|
+
const [copied, setCopied] = useState(false)
|
|
69
|
+
const copy = () => {
|
|
70
|
+
navigator.clipboard?.writeText("npx create-spine@latest").catch(() => {})
|
|
71
|
+
setCopied(true)
|
|
72
|
+
setTimeout(() => setCopied(false), 1800)
|
|
73
|
+
}
|
|
74
|
+
return (
|
|
75
|
+
<div className="flex items-center gap-2 bg-zinc-950 border border-zinc-800 rounded-lg px-3 py-2 mt-3 max-w-xs">
|
|
76
|
+
<span className="text-emerald-400 font-mono text-xs">$</span>
|
|
77
|
+
<span className="text-zinc-400 font-mono text-xs flex-1 truncate">npx create-spine@latest</span>
|
|
78
|
+
<button onClick={copy} className="text-zinc-500 hover:text-zinc-200 transition-colors shrink-0" aria-label="Copy command">
|
|
79
|
+
{copied ? <Check className="w-3.5 h-3.5 text-emerald-400" /> : <Copy className="w-3.5 h-3.5" />}
|
|
80
|
+
</button>
|
|
81
|
+
</div>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function FaqItem({ item, isOpen, onToggle }) {
|
|
86
|
+
return (
|
|
87
|
+
<div className={cx("border rounded-lg bg-zinc-900 overflow-hidden transition-colors", isOpen ? "border-zinc-700" : "border-zinc-800")}>
|
|
88
|
+
<button
|
|
89
|
+
onClick={onToggle}
|
|
90
|
+
className="w-full flex items-center justify-between gap-4 px-5 py-4 text-left hover:bg-zinc-800/30 transition-colors"
|
|
91
|
+
aria-expanded={isOpen}
|
|
92
|
+
>
|
|
93
|
+
<span className="text-sm font-medium text-zinc-100">{item.q}</span>
|
|
94
|
+
<ChevronDown className={cx("w-4 h-4 shrink-0 transition-transform duration-300", isOpen ? "rotate-180 text-emerald-400" : "text-zinc-500")} />
|
|
95
|
+
</button>
|
|
96
|
+
<div className="overflow-hidden transition-all duration-300 ease-in-out" style={{ maxHeight: isOpen ? 400 : 0, opacity: isOpen ? 1 : 0 }}>
|
|
97
|
+
<div className="px-5 pb-4">
|
|
98
|
+
<p className="text-sm text-zinc-400 leading-relaxed">{item.a}</p>
|
|
99
|
+
{item.hasChip && <CliChip />}
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
</div>
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function EmptyState({ query }) {
|
|
107
|
+
return (
|
|
108
|
+
<div className="flex flex-col items-center text-center py-16 px-6 border border-dashed border-zinc-800 rounded-lg">
|
|
109
|
+
<div className="w-10 h-10 rounded-lg bg-zinc-800 flex items-center justify-center mb-3">
|
|
110
|
+
<Search className="w-4 h-4 text-zinc-500" />
|
|
111
|
+
</div>
|
|
112
|
+
<p className="text-sm text-zinc-400 mb-1">No results for "{query}"</p>
|
|
113
|
+
<p className="text-xs text-zinc-600">Try a different search, or browse by category below.</p>
|
|
114
|
+
</div>
|
|
115
|
+
)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/* ════════════════════════════════════════════════════════════════
|
|
119
|
+
PAGE
|
|
120
|
+
════════════════════════════════════════════════════════════════ */
|
|
121
|
+
export default function FaqPage() {
|
|
122
|
+
const [query, setQuery] = useState("")
|
|
123
|
+
const [activeCategory, setActiveCategory] = useState("all")
|
|
124
|
+
const [openIds, setOpenIds] = useState(new Set())
|
|
125
|
+
|
|
126
|
+
const toggle = (id) => {
|
|
127
|
+
setOpenIds((prev) => {
|
|
128
|
+
const next = new Set(prev)
|
|
129
|
+
if (next.has(id)) next.delete(id)
|
|
130
|
+
else next.add(id)
|
|
131
|
+
return next
|
|
132
|
+
})
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const searching = query.trim().length > 0
|
|
136
|
+
|
|
137
|
+
const filtered = useMemo(() => {
|
|
138
|
+
if (searching) {
|
|
139
|
+
const q = query.trim().toLowerCase()
|
|
140
|
+
return FAQ_ITEMS.filter((i) => i.q.toLowerCase().includes(q) || i.a.toLowerCase().includes(q))
|
|
141
|
+
}
|
|
142
|
+
if (activeCategory !== "all") return FAQ_ITEMS.filter((i) => i.category === activeCategory)
|
|
143
|
+
return FAQ_ITEMS
|
|
144
|
+
}, [query, activeCategory, searching])
|
|
145
|
+
|
|
146
|
+
const grouped = useMemo(() => {
|
|
147
|
+
const map = {}
|
|
148
|
+
FAQ_CATEGORIES.forEach((c) => { map[c.id] = [] })
|
|
149
|
+
filtered.forEach((item) => { if (map[item.category]) map[item.category].push(item) })
|
|
150
|
+
return map
|
|
151
|
+
}, [filtered])
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div className="bg-black min-h-screen text-zinc-50 font-sans antialiased">
|
|
155
|
+
<style>{`
|
|
156
|
+
@media (prefers-reduced-motion: reduce) {
|
|
157
|
+
*, *::before, *::after {
|
|
158
|
+
animation-duration: 0.01ms !important;
|
|
159
|
+
animation-iteration-count: 1 !important;
|
|
160
|
+
transition-duration: 0.01ms !important;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
`}</style>
|
|
164
|
+
|
|
165
|
+
{/* ── NAV ─────────────────────────────────────────────── */}
|
|
166
|
+
<nav className="sticky top-0 z-50 h-14 flex items-center justify-between px-5 md:px-12 bg-black/85 backdrop-blur-md border-b border-zinc-800">
|
|
167
|
+
<a href="/" className="flex items-center gap-2.5">
|
|
168
|
+
<svg width="20" height="20" viewBox="0 0 24 24" fill="none">
|
|
169
|
+
<line x1="12" y1="1" x2="12" y2="23" stroke="#34d399" strokeWidth="2" strokeLinecap="round" />
|
|
170
|
+
<line x1="12" y1="5" x2="18" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
|
|
171
|
+
<line x1="12" y1="5" x2="6" y2="3" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
|
|
172
|
+
<line x1="12" y1="11" x2="18" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
|
|
173
|
+
<line x1="12" y1="11" x2="6" y2="9" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
|
|
174
|
+
<line x1="12" y1="17" x2="18" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" />
|
|
175
|
+
<line x1="12" y1="17" x2="6" y2="15" stroke="#34d399" strokeWidth="1.4" strokeLinecap="round" opacity="0.45" />
|
|
176
|
+
</svg>
|
|
177
|
+
<span className="font-bold text-base tracking-tight">Spine</span>
|
|
178
|
+
</a>
|
|
179
|
+
<div className="hidden md:flex items-center gap-7">
|
|
180
|
+
<a href="/#templates" className="text-sm text-zinc-500 hover:text-zinc-100 transition-colors">Templates</a>
|
|
181
|
+
<a href="/#pricing" className="text-sm text-zinc-500 hover:text-zinc-100 transition-colors">Pricing</a>
|
|
182
|
+
<span className="text-sm text-emerald-400">FAQ</span>
|
|
183
|
+
</div>
|
|
184
|
+
<div className="flex items-center gap-2">
|
|
185
|
+
<a href="https://github.com" className="text-zinc-500 hover:text-zinc-200 transition-colors">
|
|
186
|
+
<GithubIcon className="w-4 h-4" />
|
|
187
|
+
</a>
|
|
188
|
+
<Button
|
|
189
|
+
onClick={() => { window.location.href = "/" }}
|
|
190
|
+
size="sm"
|
|
191
|
+
className="bg-emerald-500 hover:bg-emerald-400 text-black font-semibold text-xs h-8"
|
|
192
|
+
>
|
|
193
|
+
Get started
|
|
194
|
+
</Button>
|
|
195
|
+
</div>
|
|
196
|
+
</nav>
|
|
197
|
+
|
|
198
|
+
{/* ── HERO + SEARCH ───────────────────────────────────── */}
|
|
199
|
+
<section className="relative overflow-hidden text-center px-5 md:px-12 py-16 md:py-20 border-b border-zinc-800">
|
|
200
|
+
<div
|
|
201
|
+
className="absolute top-10 left-1/2 -translate-x-1/2 pointer-events-none -z-10"
|
|
202
|
+
style={{ width: 500, height: 260, background: "radial-gradient(ellipse at center, rgba(16,185,129,0.08) 0%, transparent 70%)" }}
|
|
203
|
+
/>
|
|
204
|
+
<div className="relative max-w-2xl mx-auto">
|
|
205
|
+
<p className="text-xs text-zinc-500 tracking-widest uppercase mb-4">Support</p>
|
|
206
|
+
<h1 className="text-3xl md:text-5xl font-extrabold tracking-tighter mb-4">Frequently asked questions</h1>
|
|
207
|
+
<p className="text-sm md:text-base text-zinc-500 leading-relaxed max-w-lg mx-auto mb-8">
|
|
208
|
+
Everything you need to know about generating, customizing, and shipping with Spine. Can't find it here? Reach out below.
|
|
209
|
+
</p>
|
|
210
|
+
<div className="relative max-w-md mx-auto">
|
|
211
|
+
<Search className="absolute left-3.5 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-500 pointer-events-none" />
|
|
212
|
+
<Input
|
|
213
|
+
value={query}
|
|
214
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
215
|
+
placeholder="Search questions..."
|
|
216
|
+
aria-label="Search questions"
|
|
217
|
+
className="h-11 pl-10 bg-zinc-900 border-zinc-800 text-sm placeholder:text-zinc-600 focus-visible:ring-emerald-600"
|
|
218
|
+
/>
|
|
219
|
+
</div>
|
|
220
|
+
</div>
|
|
221
|
+
</section>
|
|
222
|
+
|
|
223
|
+
{/* ── CATEGORIES + LIST ───────────────────────────────── */}
|
|
224
|
+
<section className="max-w-3xl mx-auto px-5 md:px-12 py-14 md:py-20">
|
|
225
|
+
{!searching && (
|
|
226
|
+
<div className="flex flex-wrap justify-center gap-2 mb-12">
|
|
227
|
+
<button
|
|
228
|
+
onClick={() => setActiveCategory("all")}
|
|
229
|
+
className={cx(
|
|
230
|
+
"px-3.5 py-1.5 rounded-full text-xs font-medium border transition-colors",
|
|
231
|
+
activeCategory === "all"
|
|
232
|
+
? "bg-emerald-950 text-emerald-400 border-emerald-800"
|
|
233
|
+
: "text-zinc-400 border-zinc-800 hover:border-zinc-700 hover:text-zinc-200"
|
|
234
|
+
)}
|
|
235
|
+
>
|
|
236
|
+
All
|
|
237
|
+
</button>
|
|
238
|
+
{FAQ_CATEGORIES.map((cat) => {
|
|
239
|
+
const Icon = cat.icon
|
|
240
|
+
const active = activeCategory === cat.id
|
|
241
|
+
return (
|
|
242
|
+
<button
|
|
243
|
+
key={cat.id}
|
|
244
|
+
onClick={() => setActiveCategory(cat.id)}
|
|
245
|
+
className={cx(
|
|
246
|
+
"inline-flex items-center gap-1.5 px-3.5 py-1.5 rounded-full text-xs font-medium border transition-colors",
|
|
247
|
+
active
|
|
248
|
+
? "bg-emerald-950 text-emerald-400 border-emerald-800"
|
|
249
|
+
: "text-zinc-400 border-zinc-800 hover:border-zinc-700 hover:text-zinc-200"
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
<Icon className="w-3 h-3" /> {cat.label}
|
|
253
|
+
</button>
|
|
254
|
+
)
|
|
255
|
+
})}
|
|
256
|
+
</div>
|
|
257
|
+
)}
|
|
258
|
+
|
|
259
|
+
{searching ? (
|
|
260
|
+
<div className="flex flex-col gap-3">
|
|
261
|
+
{filtered.length === 0 ? (
|
|
262
|
+
<EmptyState query={query.trim()} />
|
|
263
|
+
) : (
|
|
264
|
+
filtered.map((item) => (
|
|
265
|
+
<FaqItem key={item.id} item={item} isOpen={openIds.has(item.id)} onToggle={() => toggle(item.id)} />
|
|
266
|
+
))
|
|
267
|
+
)}
|
|
268
|
+
</div>
|
|
269
|
+
) : (
|
|
270
|
+
<div className="flex flex-col gap-10">
|
|
271
|
+
{FAQ_CATEGORIES.filter((c) => activeCategory === "all" || activeCategory === c.id).map((cat) => {
|
|
272
|
+
const items = grouped[cat.id]
|
|
273
|
+
if (!items || items.length === 0) return null
|
|
274
|
+
const Icon = cat.icon
|
|
275
|
+
return (
|
|
276
|
+
<div key={cat.id}>
|
|
277
|
+
<div className="flex items-center gap-2 mb-4">
|
|
278
|
+
<div className="w-7 h-7 rounded-md bg-emerald-950 border border-emerald-800 flex items-center justify-center">
|
|
279
|
+
<Icon className="w-3.5 h-3.5 text-emerald-400" />
|
|
280
|
+
</div>
|
|
281
|
+
<h2 className="text-sm font-bold tracking-tight text-zinc-100">{cat.label}</h2>
|
|
282
|
+
</div>
|
|
283
|
+
<div className="flex flex-col gap-3">
|
|
284
|
+
{items.map((item) => (
|
|
285
|
+
<FaqItem key={item.id} item={item} isOpen={openIds.has(item.id)} onToggle={() => toggle(item.id)} />
|
|
286
|
+
))}
|
|
287
|
+
</div>
|
|
288
|
+
</div>
|
|
289
|
+
)
|
|
290
|
+
})}
|
|
291
|
+
</div>
|
|
292
|
+
)}
|
|
293
|
+
</section>
|
|
294
|
+
|
|
295
|
+
{/* ── CTA ─────────────────────────────────────────────── */}
|
|
296
|
+
<section className="max-w-6xl mx-auto px-5 md:px-12 py-16 md:py-24">
|
|
297
|
+
<Card className="bg-zinc-900 border-zinc-800 relative overflow-hidden">
|
|
298
|
+
<div
|
|
299
|
+
className="absolute top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 pointer-events-none"
|
|
300
|
+
style={{ width: 500, height: 260, background: "radial-gradient(ellipse at center, rgba(16,185,129,0.08) 0%, transparent 70%)" }}
|
|
301
|
+
/>
|
|
302
|
+
<CardContent className="p-10 md:p-16 text-center relative">
|
|
303
|
+
<h2 className="text-2xl md:text-4xl font-extrabold tracking-tighter mb-3 leading-tight">Still have questions?</h2>
|
|
304
|
+
<p className="text-sm text-zinc-500 mb-8">Can't find the answer you're looking for? We're happy to help.</p>
|
|
305
|
+
<div className="flex justify-center gap-2.5 flex-wrap">
|
|
306
|
+
<Button
|
|
307
|
+
onClick={() => { window.location.href = "mailto:support@spine.io" }}
|
|
308
|
+
className="bg-emerald-500 hover:bg-emerald-400 text-black font-semibold px-6"
|
|
309
|
+
>
|
|
310
|
+
Contact support
|
|
311
|
+
</Button>
|
|
312
|
+
<Button
|
|
313
|
+
variant="outline"
|
|
314
|
+
onClick={() => window.open("https://github.com", "_blank", "noopener,noreferrer")}
|
|
315
|
+
className="border-zinc-700 text-zinc-400 gap-2"
|
|
316
|
+
>
|
|
317
|
+
<GithubIcon className="w-4 h-4" /> Ask on GitHub
|
|
318
|
+
</Button>
|
|
319
|
+
</div>
|
|
320
|
+
</CardContent>
|
|
321
|
+
</Card>
|
|
322
|
+
</section>
|
|
323
|
+
|
|
324
|
+
{/* ── FOOTER ──────────────────────────────────────────── */}
|
|
325
|
+
<footer className="border-t border-zinc-800">
|
|
326
|
+
<div className="max-w-6xl mx-auto px-5 md:px-12 py-6 flex flex-col md:flex-row items-start md:items-center justify-between gap-3">
|
|
327
|
+
<span className="text-xs text-zinc-600">Spine · spine.io © {new Date().getFullYear()}</span>
|
|
328
|
+
<div className="flex gap-6">
|
|
329
|
+
{[
|
|
330
|
+
{ label: "Templates", href: "/#templates" },
|
|
331
|
+
{ label: "Pricing", href: "/#pricing" },
|
|
332
|
+
{ label: "FAQ", href: "/faq" },
|
|
333
|
+
{ label: "Docs", href: "#" },
|
|
334
|
+
{ label: "GitHub", href: "https://github.com" },
|
|
335
|
+
].map((l) => (
|
|
336
|
+
<a key={l.label} href={l.href} className="text-xs text-zinc-600 hover:text-zinc-400 transition-colors">{l.label}</a>
|
|
337
|
+
))}
|
|
338
|
+
</div>
|
|
339
|
+
</div>
|
|
340
|
+
</footer>
|
|
341
|
+
</div>
|
|
342
|
+
)
|
|
343
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@import "tw-animate-css";
|
|
3
|
+
@import "shadcn/tailwind.css";
|
|
4
|
+
|
|
5
|
+
@custom-variant dark (&:is(.dark *));
|
|
6
|
+
|
|
7
|
+
@theme inline {
|
|
8
|
+
--color-background: var(--background);
|
|
9
|
+
--color-foreground: var(--foreground);
|
|
10
|
+
--font-sans: var(--font-sans);
|
|
11
|
+
--font-mono: var(--font-geist-mono);
|
|
12
|
+
--font-heading: var(--font-sans);
|
|
13
|
+
--color-sidebar-ring: var(--sidebar-ring);
|
|
14
|
+
--color-sidebar-border: var(--sidebar-border);
|
|
15
|
+
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
|
16
|
+
--color-sidebar-accent: var(--sidebar-accent);
|
|
17
|
+
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
|
|
18
|
+
--color-sidebar-primary: var(--sidebar-primary);
|
|
19
|
+
--color-sidebar-foreground: var(--sidebar-foreground);
|
|
20
|
+
--color-sidebar: var(--sidebar);
|
|
21
|
+
--color-chart-5: var(--chart-5);
|
|
22
|
+
--color-chart-4: var(--chart-4);
|
|
23
|
+
--color-chart-3: var(--chart-3);
|
|
24
|
+
--color-chart-2: var(--chart-2);
|
|
25
|
+
--color-chart-1: var(--chart-1);
|
|
26
|
+
--color-ring: var(--ring);
|
|
27
|
+
--color-input: var(--input);
|
|
28
|
+
--color-border: var(--border);
|
|
29
|
+
--color-destructive: var(--destructive);
|
|
30
|
+
--color-accent-foreground: var(--accent-foreground);
|
|
31
|
+
--color-accent: var(--accent);
|
|
32
|
+
--color-muted-foreground: var(--muted-foreground);
|
|
33
|
+
--color-muted: var(--muted);
|
|
34
|
+
--color-secondary-foreground: var(--secondary-foreground);
|
|
35
|
+
--color-secondary: var(--secondary);
|
|
36
|
+
--color-primary-foreground: var(--primary-foreground);
|
|
37
|
+
--color-primary: var(--primary);
|
|
38
|
+
--color-popover-foreground: var(--popover-foreground);
|
|
39
|
+
--color-popover: var(--popover);
|
|
40
|
+
--color-card-foreground: var(--card-foreground);
|
|
41
|
+
--color-card: var(--card);
|
|
42
|
+
--radius-sm: calc(var(--radius) * 0.6);
|
|
43
|
+
--radius-md: calc(var(--radius) * 0.8);
|
|
44
|
+
--radius-lg: var(--radius);
|
|
45
|
+
--radius-xl: calc(var(--radius) * 1.4);
|
|
46
|
+
--radius-2xl: calc(var(--radius) * 1.8);
|
|
47
|
+
--radius-3xl: calc(var(--radius) * 2.2);
|
|
48
|
+
--radius-4xl: calc(var(--radius) * 2.6);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
:root {
|
|
52
|
+
--background: oklch(1 0 0);
|
|
53
|
+
--foreground: oklch(0.145 0 0);
|
|
54
|
+
--card: oklch(1 0 0);
|
|
55
|
+
--card-foreground: oklch(0.145 0 0);
|
|
56
|
+
--popover: oklch(1 0 0);
|
|
57
|
+
--popover-foreground: oklch(0.145 0 0);
|
|
58
|
+
--primary: oklch(0.205 0 0);
|
|
59
|
+
--primary-foreground: oklch(0.985 0 0);
|
|
60
|
+
--secondary: oklch(0.97 0 0);
|
|
61
|
+
--secondary-foreground: oklch(0.205 0 0);
|
|
62
|
+
--muted: oklch(0.97 0 0);
|
|
63
|
+
--muted-foreground: oklch(0.556 0 0);
|
|
64
|
+
--accent: oklch(0.97 0 0);
|
|
65
|
+
--accent-foreground: oklch(0.205 0 0);
|
|
66
|
+
--destructive: oklch(0.577 0.245 27.325);
|
|
67
|
+
--border: oklch(0.922 0 0);
|
|
68
|
+
--input: oklch(0.922 0 0);
|
|
69
|
+
--ring: oklch(0.708 0 0);
|
|
70
|
+
--chart-1: oklch(0.87 0 0);
|
|
71
|
+
--chart-2: oklch(0.556 0 0);
|
|
72
|
+
--chart-3: oklch(0.439 0 0);
|
|
73
|
+
--chart-4: oklch(0.371 0 0);
|
|
74
|
+
--chart-5: oklch(0.269 0 0);
|
|
75
|
+
--radius: 0.625rem;
|
|
76
|
+
--sidebar: oklch(0.985 0 0);
|
|
77
|
+
--sidebar-foreground: oklch(0.145 0 0);
|
|
78
|
+
--sidebar-primary: oklch(0.205 0 0);
|
|
79
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
80
|
+
--sidebar-accent: oklch(0.97 0 0);
|
|
81
|
+
--sidebar-accent-foreground: oklch(0.205 0 0);
|
|
82
|
+
--sidebar-border: oklch(0.922 0 0);
|
|
83
|
+
--sidebar-ring: oklch(0.708 0 0);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.dark {
|
|
87
|
+
--background: oklch(0.145 0 0);
|
|
88
|
+
--foreground: oklch(0.985 0 0);
|
|
89
|
+
--card: oklch(0.205 0 0);
|
|
90
|
+
--card-foreground: oklch(0.985 0 0);
|
|
91
|
+
--popover: oklch(0.205 0 0);
|
|
92
|
+
--popover-foreground: oklch(0.985 0 0);
|
|
93
|
+
--primary: oklch(0.922 0 0);
|
|
94
|
+
--primary-foreground: oklch(0.205 0 0);
|
|
95
|
+
--secondary: oklch(0.269 0 0);
|
|
96
|
+
--secondary-foreground: oklch(0.985 0 0);
|
|
97
|
+
--muted: oklch(0.269 0 0);
|
|
98
|
+
--muted-foreground: oklch(0.708 0 0);
|
|
99
|
+
--accent: oklch(0.269 0 0);
|
|
100
|
+
--accent-foreground: oklch(0.985 0 0);
|
|
101
|
+
--destructive: oklch(0.704 0.191 22.216);
|
|
102
|
+
--border: oklch(1 0 0 / 10%);
|
|
103
|
+
--input: oklch(1 0 0 / 15%);
|
|
104
|
+
--ring: oklch(0.556 0 0);
|
|
105
|
+
--chart-1: oklch(0.87 0 0);
|
|
106
|
+
--chart-2: oklch(0.556 0 0);
|
|
107
|
+
--chart-3: oklch(0.439 0 0);
|
|
108
|
+
--chart-4: oklch(0.371 0 0);
|
|
109
|
+
--chart-5: oklch(0.269 0 0);
|
|
110
|
+
--sidebar: oklch(0.205 0 0);
|
|
111
|
+
--sidebar-foreground: oklch(0.985 0 0);
|
|
112
|
+
--sidebar-primary: oklch(0.488 0.243 264.376);
|
|
113
|
+
--sidebar-primary-foreground: oklch(0.985 0 0);
|
|
114
|
+
--sidebar-accent: oklch(0.269 0 0);
|
|
115
|
+
--sidebar-accent-foreground: oklch(0.985 0 0);
|
|
116
|
+
--sidebar-border: oklch(1 0 0 / 10%);
|
|
117
|
+
--sidebar-ring: oklch(0.556 0 0);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@layer base {
|
|
121
|
+
* {
|
|
122
|
+
@apply border-border outline-ring/50;
|
|
123
|
+
}
|
|
124
|
+
body {
|
|
125
|
+
@apply bg-background text-foreground;
|
|
126
|
+
}
|
|
127
|
+
html {
|
|
128
|
+
@apply font-sans;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Geist, Geist_Mono } from "next/font/google";
|
|
3
|
+
import "./globals.css";
|
|
4
|
+
|
|
5
|
+
const geistSans = Geist({
|
|
6
|
+
variable: "--font-geist-sans",
|
|
7
|
+
subsets: ["latin"],
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const geistMono = Geist_Mono({
|
|
11
|
+
variable: "--font-geist-mono",
|
|
12
|
+
subsets: ["latin"],
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const metadata: Metadata = {
|
|
16
|
+
title: "Create Next App",
|
|
17
|
+
description: "Generated by create next app",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export default function RootLayout({
|
|
21
|
+
children,
|
|
22
|
+
}: Readonly<{
|
|
23
|
+
children: React.ReactNode;
|
|
24
|
+
}>) {
|
|
25
|
+
return (
|
|
26
|
+
<html
|
|
27
|
+
lang="en"
|
|
28
|
+
className={`${geistSans.variable} ${geistMono.variable} h-full antialiased`}
|
|
29
|
+
>
|
|
30
|
+
<body className="min-h-full flex flex-col">{children}</body>
|
|
31
|
+
</html>
|
|
32
|
+
);
|
|
33
|
+
}
|