create-mantiq 0.0.1
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 +19 -0
- package/package.json +49 -0
- package/src/index.ts +179 -0
- package/src/kits/react.ts +368 -0
- package/src/kits/svelte.ts +337 -0
- package/src/kits/vue.ts +346 -0
- package/src/templates.ts +997 -0
package/src/kits/vue.ts
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import type { TemplateContext } from '../templates.ts'
|
|
2
|
+
|
|
3
|
+
export function getVueTemplates(ctx: TemplateContext): Record<string, string> {
|
|
4
|
+
return {
|
|
5
|
+
'vite.config.ts': `import { defineConfig } from 'vite'
|
|
6
|
+
import vue from '@vitejs/plugin-vue'
|
|
7
|
+
import tailwindcss from '@tailwindcss/vite'
|
|
8
|
+
|
|
9
|
+
export default defineConfig({
|
|
10
|
+
plugins: [vue(), tailwindcss()],
|
|
11
|
+
publicDir: false,
|
|
12
|
+
build: {
|
|
13
|
+
outDir: 'public/build',
|
|
14
|
+
manifest: true,
|
|
15
|
+
emptyOutDir: true,
|
|
16
|
+
rollupOptions: {
|
|
17
|
+
input: ['src/main.ts', 'src/style.css'],
|
|
18
|
+
},
|
|
19
|
+
},
|
|
20
|
+
})
|
|
21
|
+
`,
|
|
22
|
+
|
|
23
|
+
'src/style.css': `@import "tailwindcss";
|
|
24
|
+
`,
|
|
25
|
+
|
|
26
|
+
'src/pages.ts': `import Login from './pages/Login.vue'
|
|
27
|
+
import Register from './pages/Register.vue'
|
|
28
|
+
import Dashboard from './pages/Dashboard.vue'
|
|
29
|
+
|
|
30
|
+
export const pages: Record<string, any> = {
|
|
31
|
+
Login,
|
|
32
|
+
Register,
|
|
33
|
+
Dashboard,
|
|
34
|
+
}
|
|
35
|
+
`,
|
|
36
|
+
|
|
37
|
+
'src/lib/api.ts': `export async function api<T = any>(url: string, opts: RequestInit = {}): Promise<{ ok: boolean; status: number; data: T }> {
|
|
38
|
+
const res = await fetch(url, { ...opts, headers: { Accept: 'application/json', ...opts.headers } })
|
|
39
|
+
const data = res.headers.get('content-type')?.includes('json') ? await res.json() : null
|
|
40
|
+
return { ok: res.ok, status: res.status, data }
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function post(url: string, body: object) {
|
|
44
|
+
return api(url, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) })
|
|
45
|
+
}
|
|
46
|
+
`,
|
|
47
|
+
|
|
48
|
+
'src/main.ts': `import './style.css'
|
|
49
|
+
import { createApp, createSSRApp } from 'vue'
|
|
50
|
+
import App from './App.vue'
|
|
51
|
+
import { pages } from './pages.ts'
|
|
52
|
+
|
|
53
|
+
const root = document.getElementById('app')!
|
|
54
|
+
const data = (window as any).__MANTIQ_DATA__ ?? {}
|
|
55
|
+
|
|
56
|
+
const app = root.innerHTML.trim()
|
|
57
|
+
? createSSRApp(App, { pages, initialData: data })
|
|
58
|
+
: createApp(App, { pages, initialData: data })
|
|
59
|
+
|
|
60
|
+
app.mount('#app')
|
|
61
|
+
`,
|
|
62
|
+
|
|
63
|
+
'src/ssr.ts': `import { createSSRApp } from 'vue'
|
|
64
|
+
import { renderToString } from 'vue/server-renderer'
|
|
65
|
+
import App from './App.vue'
|
|
66
|
+
import { pages } from './pages.ts'
|
|
67
|
+
|
|
68
|
+
export async function render(_url: string, data?: Record<string, any>) {
|
|
69
|
+
const app = createSSRApp(App, { pages, initialData: data })
|
|
70
|
+
return { html: await renderToString(app) }
|
|
71
|
+
}
|
|
72
|
+
`,
|
|
73
|
+
|
|
74
|
+
'src/App.vue': `<script setup lang="ts">
|
|
75
|
+
import { ref, shallowRef, onMounted, onUnmounted, provide } from 'vue'
|
|
76
|
+
|
|
77
|
+
const props = defineProps<{
|
|
78
|
+
pages: Record<string, any>
|
|
79
|
+
initialData?: Record<string, any>
|
|
80
|
+
}>()
|
|
81
|
+
|
|
82
|
+
const windowData = typeof window !== 'undefined' ? (window as any).__MANTIQ_DATA__ : {}
|
|
83
|
+
const initial = props.initialData ?? windowData
|
|
84
|
+
|
|
85
|
+
const currentPage = ref<string>(initial._page ?? 'Login')
|
|
86
|
+
const pageData = ref<Record<string, any>>(initial)
|
|
87
|
+
const PageComponent = shallowRef(props.pages[currentPage.value] ?? null)
|
|
88
|
+
|
|
89
|
+
async function navigate(href: string) {
|
|
90
|
+
const res = await fetch(href, {
|
|
91
|
+
headers: { 'X-Mantiq': 'true', Accept: 'application/json' },
|
|
92
|
+
})
|
|
93
|
+
const newData = await res.json()
|
|
94
|
+
currentPage.value = newData._page
|
|
95
|
+
pageData.value = newData
|
|
96
|
+
PageComponent.value = props.pages[newData._page] ?? null
|
|
97
|
+
history.pushState(null, '', newData._url)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
provide('navigate', navigate)
|
|
101
|
+
|
|
102
|
+
function handleClick(e: MouseEvent) {
|
|
103
|
+
const anchor = (e.target as HTMLElement).closest('a')
|
|
104
|
+
const href = anchor?.getAttribute('href')
|
|
105
|
+
if (!href?.startsWith('/') || anchor?.target || e.ctrlKey || e.metaKey) return
|
|
106
|
+
e.preventDefault()
|
|
107
|
+
navigate(href)
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function handlePop() { navigate(location.pathname) }
|
|
111
|
+
|
|
112
|
+
onMounted(() => {
|
|
113
|
+
document.addEventListener('click', handleClick)
|
|
114
|
+
window.addEventListener('popstate', handlePop)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
onUnmounted(() => {
|
|
118
|
+
document.removeEventListener('click', handleClick)
|
|
119
|
+
window.removeEventListener('popstate', handlePop)
|
|
120
|
+
})
|
|
121
|
+
</script>
|
|
122
|
+
|
|
123
|
+
<template>
|
|
124
|
+
<component :is="PageComponent" v-bind="pageData" :navigate="navigate" v-if="PageComponent" />
|
|
125
|
+
</template>
|
|
126
|
+
`,
|
|
127
|
+
|
|
128
|
+
'src/pages/Login.vue': `<script setup lang="ts">
|
|
129
|
+
import { ref } from 'vue'
|
|
130
|
+
import { post } from '../lib/api.ts'
|
|
131
|
+
|
|
132
|
+
const props = defineProps<{
|
|
133
|
+
appName?: string
|
|
134
|
+
navigate: (href: string) => void
|
|
135
|
+
}>()
|
|
136
|
+
|
|
137
|
+
const appName = props.appName ?? '${ctx.name}'
|
|
138
|
+
const email = ref('admin@example.com')
|
|
139
|
+
const password = ref('password')
|
|
140
|
+
const error = ref('')
|
|
141
|
+
const loading = ref(false)
|
|
142
|
+
|
|
143
|
+
async function handleSubmit() {
|
|
144
|
+
error.value = ''; loading.value = true
|
|
145
|
+
const { ok, data } = await post('/login', { email: email.value, password: password.value })
|
|
146
|
+
if (ok) props.navigate('/dashboard')
|
|
147
|
+
else error.value = data?.error ?? 'Login failed'
|
|
148
|
+
loading.value = false
|
|
149
|
+
}
|
|
150
|
+
</script>
|
|
151
|
+
|
|
152
|
+
<template>
|
|
153
|
+
<div class="min-h-screen bg-gray-950 flex">
|
|
154
|
+
<div class="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-indigo-950 via-gray-950 to-gray-950 items-center justify-center p-16 relative overflow-hidden">
|
|
155
|
+
<div class="absolute inset-0 bg-[radial-gradient(circle_at_30%_40%,rgba(99,102,241,0.08),transparent_60%)]" />
|
|
156
|
+
<div class="relative space-y-6 max-w-md">
|
|
157
|
+
<div class="flex items-center gap-3">
|
|
158
|
+
<div class="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
159
|
+
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
160
|
+
</div>
|
|
161
|
+
<span class="text-2xl font-bold text-white">{{ appName }}</span>
|
|
162
|
+
</div>
|
|
163
|
+
<h2 class="text-4xl font-bold text-white leading-tight">Build something<br>amazing.</h2>
|
|
164
|
+
<p class="text-gray-400 text-lg leading-relaxed">Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.</p>
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
<div class="flex-1 flex items-center justify-center p-8">
|
|
168
|
+
<div class="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
|
|
169
|
+
<div>
|
|
170
|
+
<h1 class="text-xl font-bold text-white">Welcome back</h1>
|
|
171
|
+
<p class="text-sm text-gray-500 mt-1">Sign in to your account</p>
|
|
172
|
+
</div>
|
|
173
|
+
<div v-if="error" class="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{{ error }}</div>
|
|
174
|
+
<form @submit.prevent="handleSubmit" class="space-y-4">
|
|
175
|
+
<div class="space-y-1">
|
|
176
|
+
<label class="block text-sm font-medium text-gray-400">Email</label>
|
|
177
|
+
<input v-model="email" type="email" required class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
|
|
178
|
+
</div>
|
|
179
|
+
<div class="space-y-1">
|
|
180
|
+
<label class="block text-sm font-medium text-gray-400">Password</label>
|
|
181
|
+
<input v-model="password" type="password" required class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
|
|
182
|
+
</div>
|
|
183
|
+
<button type="submit" :disabled="loading" class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">Sign in</button>
|
|
184
|
+
</form>
|
|
185
|
+
<p class="text-sm text-gray-500 text-center">Don't have an account? <a href="/register" class="text-indigo-400 hover:text-indigo-300">Register</a></p>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
</div>
|
|
189
|
+
</template>
|
|
190
|
+
`,
|
|
191
|
+
|
|
192
|
+
'src/pages/Register.vue': `<script setup lang="ts">
|
|
193
|
+
import { ref } from 'vue'
|
|
194
|
+
import { post } from '../lib/api.ts'
|
|
195
|
+
|
|
196
|
+
const props = defineProps<{
|
|
197
|
+
appName?: string
|
|
198
|
+
navigate: (href: string) => void
|
|
199
|
+
}>()
|
|
200
|
+
|
|
201
|
+
const appName = props.appName ?? '${ctx.name}'
|
|
202
|
+
const name = ref('')
|
|
203
|
+
const email = ref('')
|
|
204
|
+
const password = ref('')
|
|
205
|
+
const error = ref('')
|
|
206
|
+
const loading = ref(false)
|
|
207
|
+
|
|
208
|
+
async function handleSubmit() {
|
|
209
|
+
error.value = ''; loading.value = true
|
|
210
|
+
const { ok, data } = await post('/register', { name: name.value, email: email.value, password: password.value })
|
|
211
|
+
if (ok) props.navigate('/dashboard')
|
|
212
|
+
else error.value = data?.error?.message ?? data?.error ?? 'Registration failed'
|
|
213
|
+
loading.value = false
|
|
214
|
+
}
|
|
215
|
+
</script>
|
|
216
|
+
|
|
217
|
+
<template>
|
|
218
|
+
<div class="min-h-screen bg-gray-950 flex">
|
|
219
|
+
<div class="hidden lg:flex lg:w-1/2 bg-gradient-to-br from-indigo-950 via-gray-950 to-gray-950 items-center justify-center p-16 relative overflow-hidden">
|
|
220
|
+
<div class="absolute inset-0 bg-[radial-gradient(circle_at_30%_40%,rgba(99,102,241,0.08),transparent_60%)]" />
|
|
221
|
+
<div class="relative space-y-6 max-w-md">
|
|
222
|
+
<div class="flex items-center gap-3">
|
|
223
|
+
<div class="w-12 h-12 rounded-xl bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
224
|
+
<svg class="w-6 h-6 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
225
|
+
</div>
|
|
226
|
+
<span class="text-2xl font-bold text-white">{{ appName }}</span>
|
|
227
|
+
</div>
|
|
228
|
+
<h2 class="text-4xl font-bold text-white leading-tight">Build something<br>amazing.</h2>
|
|
229
|
+
<p class="text-gray-400 text-lg leading-relaxed">Session auth, encrypted cookies, CSRF protection — all wired up and ready to go.</p>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
<div class="flex-1 flex items-center justify-center p-8">
|
|
233
|
+
<div class="bg-gray-900 rounded-xl border border-gray-800 w-full max-w-md p-8 space-y-6">
|
|
234
|
+
<div>
|
|
235
|
+
<h1 class="text-xl font-bold text-white">Create an account</h1>
|
|
236
|
+
<p class="text-sm text-gray-500 mt-1">Get started with {{ appName }}</p>
|
|
237
|
+
</div>
|
|
238
|
+
<div v-if="error" class="bg-red-500/10 border border-red-500/30 text-red-400 rounded-lg px-3.5 py-2.5 text-sm">{{ error }}</div>
|
|
239
|
+
<form @submit.prevent="handleSubmit" class="space-y-4">
|
|
240
|
+
<div class="space-y-1">
|
|
241
|
+
<label class="block text-sm font-medium text-gray-400">Name</label>
|
|
242
|
+
<input v-model="name" required class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
|
|
243
|
+
</div>
|
|
244
|
+
<div class="space-y-1">
|
|
245
|
+
<label class="block text-sm font-medium text-gray-400">Email</label>
|
|
246
|
+
<input v-model="email" type="email" required class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
|
|
247
|
+
</div>
|
|
248
|
+
<div class="space-y-1">
|
|
249
|
+
<label class="block text-sm font-medium text-gray-400">Password</label>
|
|
250
|
+
<input v-model="password" type="password" required class="w-full bg-gray-900 border border-gray-800 rounded-lg px-3.5 py-2.5 text-sm text-gray-100 placeholder-gray-600 focus:outline-none focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500/30 transition-all" />
|
|
251
|
+
</div>
|
|
252
|
+
<button type="submit" :disabled="loading" class="w-full bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white font-semibold text-sm py-2.5 rounded-lg transition-colors">Create account</button>
|
|
253
|
+
</form>
|
|
254
|
+
<p class="text-sm text-gray-500 text-center">Already have an account? <a href="/login" class="text-indigo-400 hover:text-indigo-300">Sign in</a></p>
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
</div>
|
|
258
|
+
</template>
|
|
259
|
+
`,
|
|
260
|
+
|
|
261
|
+
'src/pages/Dashboard.vue': `<script setup lang="ts">
|
|
262
|
+
import { ref, onMounted } from 'vue'
|
|
263
|
+
import { api, post } from '../lib/api.ts'
|
|
264
|
+
|
|
265
|
+
const props = defineProps<{
|
|
266
|
+
appName?: string
|
|
267
|
+
currentUser?: any
|
|
268
|
+
users?: any[]
|
|
269
|
+
navigate: (href: string) => void
|
|
270
|
+
}>()
|
|
271
|
+
|
|
272
|
+
const appName = props.appName ?? '${ctx.name}'
|
|
273
|
+
const users = ref(props.users ?? [])
|
|
274
|
+
const loading = ref(!props.users?.length)
|
|
275
|
+
|
|
276
|
+
async function fetchUsers() {
|
|
277
|
+
loading.value = true
|
|
278
|
+
const { ok, data } = await api('/api/users')
|
|
279
|
+
if (ok) users.value = data.data ?? []
|
|
280
|
+
loading.value = false
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function handleLogout() {
|
|
284
|
+
await post('/logout', {})
|
|
285
|
+
props.navigate('/login')
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
onMounted(() => {
|
|
289
|
+
if (!props.users?.length) fetchUsers()
|
|
290
|
+
})
|
|
291
|
+
</script>
|
|
292
|
+
|
|
293
|
+
<template>
|
|
294
|
+
<div class="min-h-screen bg-gray-950 text-gray-100">
|
|
295
|
+
<nav class="border-b border-gray-800/80 bg-gray-950/90 backdrop-blur-md sticky top-0 z-20">
|
|
296
|
+
<div class="max-w-5xl mx-auto px-6 h-14 flex items-center justify-between">
|
|
297
|
+
<div class="flex items-center gap-2.5">
|
|
298
|
+
<div class="w-7 h-7 rounded-lg bg-indigo-600/20 border border-indigo-500/30 flex items-center justify-center">
|
|
299
|
+
<svg class="w-3.5 h-3.5 text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
|
|
300
|
+
</div>
|
|
301
|
+
<span class="text-sm font-bold text-white">{{ appName }}</span>
|
|
302
|
+
</div>
|
|
303
|
+
<div class="flex items-center gap-3">
|
|
304
|
+
<span class="text-xs text-gray-400">{{ currentUser?.name }}</span>
|
|
305
|
+
<button @click="handleLogout" class="text-xs text-gray-500 hover:text-white bg-gray-900 hover:bg-gray-800 border border-gray-800 rounded-lg px-3 py-1.5 transition-colors">Logout</button>
|
|
306
|
+
</div>
|
|
307
|
+
</div>
|
|
308
|
+
</nav>
|
|
309
|
+
<main class="max-w-5xl mx-auto px-6 py-8 space-y-6">
|
|
310
|
+
<div>
|
|
311
|
+
<h1 class="text-xl font-bold text-white">Dashboard</h1>
|
|
312
|
+
<p class="text-sm text-gray-500 mt-1">Welcome back, {{ currentUser?.name }}.</p>
|
|
313
|
+
</div>
|
|
314
|
+
<div class="bg-gray-900 rounded-xl border border-gray-800 overflow-hidden">
|
|
315
|
+
<div class="px-5 py-4 border-b border-gray-800 flex items-center justify-between">
|
|
316
|
+
<h2 class="text-sm font-bold text-gray-200">Users</h2>
|
|
317
|
+
<span class="text-xs text-gray-500">{{ loading ? 'Loading...' : users.length + ' total' }}</span>
|
|
318
|
+
</div>
|
|
319
|
+
<table class="w-full text-sm">
|
|
320
|
+
<thead>
|
|
321
|
+
<tr class="border-b border-gray-800 text-left text-xs text-gray-500 uppercase tracking-wider">
|
|
322
|
+
<th class="px-5 py-3 font-medium">Name</th>
|
|
323
|
+
<th class="px-5 py-3 font-medium">Email</th>
|
|
324
|
+
<th class="px-5 py-3 font-medium">Role</th>
|
|
325
|
+
</tr>
|
|
326
|
+
</thead>
|
|
327
|
+
<tbody class="divide-y divide-gray-800/60">
|
|
328
|
+
<tr v-for="u in users" :key="u.id" class="hover:bg-gray-900/50 transition-colors">
|
|
329
|
+
<td class="px-5 py-3 text-gray-200">{{ u.name }}</td>
|
|
330
|
+
<td class="px-5 py-3 text-gray-400">{{ u.email }}</td>
|
|
331
|
+
<td class="px-5 py-3">
|
|
332
|
+
<span :class="u.role === 'admin' ? 'bg-purple-500/15 text-purple-300 border-purple-500/20' : 'bg-gray-800 text-gray-400 border-gray-700'" class="text-[10px] px-2 py-0.5 rounded-full font-medium border">{{ u.role }}</span>
|
|
333
|
+
</td>
|
|
334
|
+
</tr>
|
|
335
|
+
<tr v-if="users.length === 0 && !loading">
|
|
336
|
+
<td colspan="3" class="px-5 py-8 text-center text-gray-600">No users found</td>
|
|
337
|
+
</tr>
|
|
338
|
+
</tbody>
|
|
339
|
+
</table>
|
|
340
|
+
</div>
|
|
341
|
+
</main>
|
|
342
|
+
</div>
|
|
343
|
+
</template>
|
|
344
|
+
`,
|
|
345
|
+
}
|
|
346
|
+
}
|