@edgedev/create-edge-app 1.1.18 → 1.1.21

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/agent.md ADDED
@@ -0,0 +1,78 @@
1
+ # Edge App Agent Guide
2
+
3
+ This project is Nuxt 3 + Vue 3, SPA mode. Follow these rules so new code matches how we already build apps.
4
+
5
+ ## Core stack and style
6
+ - Use `<script setup>` with JavaScript only (no TypeScript, no Options API).
7
+ - Components and composables from `edge/composables/**` are auto-imported; avoid manual imports unless needed.
8
+ - Utilities: use `cn` from `@/lib/utils` for class merging, `lucide-vue-next` for icons, Tailwind for layout/styling. Keep comments minimal and useful.
9
+ - Components under `edge/components` are globally registered with the `edge-` prefix (e.g., `edge-dashboard`, `edge-editor`, `edge-shad-button`).
10
+
11
+ ## Firebase and data access
12
+ - Never import Firebase SDKs directly. All Auth/Firestore/Functions/Storage access goes through the injected `edgeFirebase` plugin (`plugins/firebase.client.ts` from `@edgedev/firebase`).
13
+ - Get the instance via `const edgeFirebase = inject('edgeFirebase')`. Use the wrapper methods already used in the codebase: `startSnapshot/stopSnapshot`, `startUsersSnapshot`, `SearchStaticData`, `getDocData`, `storeDoc`, `changeDoc`, `removeDoc`, `runFunction`, `setUserMeta`, `addUser`, `removeUserRoles`, etc.
14
+ - Always scope Firestore paths to the active org using `edgeGlobal.edgeState.organizationDocPath` (e.g., ``${edgeGlobal.edgeState.organizationDocPath}/things``). Organization is set via `projectSetOrg`/`edgeGlobal.setOrganization` and lives in `edgeGlobal.edgeState.currentOrganization`.
15
+ - For reads that should live-update, start a snapshot; for static one-off queries, use `new edgeFirebase.SearchStaticData().getData(path, query, order, limit)`. Clean up snapshots when appropriate.
16
+ - Auth state comes from `edgeFirebase.user` (see `edge/components/auth.vue` patterns). Use provided helpers instead of rolling your own auth flows.
17
+
18
+ ## Preferred UI building blocks
19
+ - Lists/tables: use `edge-dashboard`. Pass `collection`, `filter`/`filters`, search/sort props, and point it at the org-scoped collection path. It handles snapshots, search, pagination, delete dialogs, and navigation to item routes.
20
+ - Editing/creation: use `edge-editor`. Provide `collection`, `docId` (`'new'` when creating), `newDocSchema` (fields with default values), and optional `schema`/overrides. It manages working copy, validation hooks, unsaved-change guards, and redirects.
21
+ - Surrounding layout: `pages/app.vue` already wires the sidebar, menu, and panels. Put new dashboard/edit pages under `/pages/app/dashboard/...` and keep them lean—compose existing Edge components rather than building new scaffolding.
22
+ - Form controls: prefer the Edge shadcn wrappers in `edge/components/shad` (e.g., `edge-shad-input`, `edge-shad-select`, `edge-shad-button`) and specialized controls like `edge-g-input`, `edge-auto-file-upload`, etc., instead of raw HTML inputs.
23
+
24
+ ## Typical collection pattern
25
+ ```vue
26
+ <script setup>
27
+ const edgeFirebase = inject('edgeFirebase')
28
+ const route = useRoute()
29
+
30
+ const collection = 'things'
31
+ const docId = computed(() => route.params.docId || 'new')
32
+
33
+ const newDocSchema = {
34
+ name: { value: '' },
35
+ description: { value: '' },
36
+ }
37
+ </script>
38
+
39
+ <template>
40
+ <edge-dashboard
41
+ v-if="!route.params.docId"
42
+ :collection="collection"
43
+ class="h-full"
44
+ header-class="bg-secondary"
45
+ />
46
+
47
+ <edge-editor
48
+ v-else
49
+ :collection="collection"
50
+ :doc-id="docId"
51
+ :new-doc-schema="newDocSchema"
52
+ />
53
+ </template>
54
+ ```
55
+ Adjust props (search, filters, pagination, save overrides) using the existing component APIs rather than reinventing logic.
56
+
57
+ ### Slots and layout flexibility
58
+ - Both `edge-dashboard` and `edge-editor` expose many slots (headers, footers, actions, item rendering, etc.)—use them to build richer UI without replacing the core components. You can significantly change look/behavior via slots while keeping Edge logic intact.
59
+ - Dashboard and editor don’t need to live on the same page; feel free to separate list/detail routes or co-locate them as needed, but always compose using the Edge components.
60
+ - Nuxt page structure can be rearranged to meet requirements, as long as navigation/layout still leans on the shared Edge tools (sidebar/menu, shadcn components, dashboard/editor, cms pieces).
61
+
62
+ ## Routing, state, and menus
63
+ - Organization-aware pages assume `edgeGlobal.edgeState.currentOrganization` is set; if you need menu items, extend `edgeGlobal.edgeState.menuItems` (see `pages/app.vue`).
64
+ - Use `useState('auth')` and other Nuxt composables already present for global auth/route handling. Keep SSR considerations minimal (app runs client-side).
65
+
66
+ ## Shadcn usage
67
+ - shadcn components live under `components/ui` and Edge-wrapped variants under `edge/components/shad`. Prefer the Edge variants to keep styling consistent and take advantage of shared props/slots.
68
+ - Styling: stick to Tailwind utility classes alongside the Edge components; when building new components, use Tailwind + `cn` for class composition instead of custom CSS where possible.
69
+
70
+ ## Do/Don't
71
+ - Do reuse Edge components (`dashboard`, `editor`, `cms` blocks, auth widgets) before adding new ones.
72
+ - Do keep Firestore paths, queries, and role checks consistent with `edgeGlobal` helpers (`isAdminGlobal`, `getRoleName`, etc.).
73
+ - Don’t introduce TypeScript, Options API, raw Firebase SDK calls, or ad-hoc forms/tables when an Edge component exists.
74
+ - Don’t edit code inside the `edge` folder unless absolutely required; it is a shared repo. If a change is unavoidable, keep it generic (no project-specific hacks) and call out the suggestion instead of making the edit when possible.
75
+
76
+ ## Firebase Functions guidance
77
+ - Review `functions/config.js`, `functions/edgeFirebase.js`, and `functions/cms.js` to mirror established patterns, but do not edit those files.
78
+ - When adding new cloud functions, create a new JS file under `functions/` and export handlers using the shared imports from `config.js`. Wire it up by requiring it in `functions/index.js` (same pattern as `stripe.js`), instead of modifying restricted files.
package/firebase_init.sh CHANGED
@@ -61,7 +61,9 @@ echo "VITE_FIREBASE_MEASUREMENT_ID=$measurement_id" >> .env
61
61
  echo "VITE_FIREBASE_EMULATOR_AUTH=" >> .env
62
62
  echo "VITE_FIREBASE_EMULATOR_FIRESTORE=" >> .env
63
63
  echo "VITE_FIREBASE_EMULATOR_FUNCTIONS=" >> .env
64
+ echo "VITE_FIREBASE_EMULATOR_STORAGE=" >> .env
64
65
  echo "REGISTRATION_CODE=organization-registration-template" >> .env
66
+ echo "DEVELOPMENT_MODE=false" >> .env
65
67
 
66
68
  # Create the .env.dev file
67
69
  echo "VITE_FIREBASE_API_KEY=$api_key" > .env.dev
@@ -74,4 +76,6 @@ echo "VITE_FIREBASE_MEASUREMENT_ID=$measurement_id" >> .env.dev
74
76
  echo "VITE_FIREBASE_EMULATOR_AUTH=9099" >> .env.dev
75
77
  echo "VITE_FIREBASE_EMULATOR_FIRESTORE=8080" >> .env.dev
76
78
  echo "VITE_FIREBASE_EMULATOR_FUNCTIONS=5001" >> .env.dev
79
+ echo "VITE_FIREBASE_EMULATOR_STORAGE=9199" >> .env.dev
77
80
  echo "REGISTRATION_CODE=organization-registration-template" >> .env.dev
81
+ echo "DEVELOPMENT_MODE=true" >> .env.dev
package/nuxt.config.ts CHANGED
@@ -4,6 +4,7 @@ export default defineNuxtConfig({
4
4
  runtimeConfig: {
5
5
  public: {
6
6
  registrationCode: process.env.REGISTRATION_CODE,
7
+ developmentMode: process.env.DEVELOPMENT_MODE === 'true',
7
8
  },
8
9
  },
9
10
  app: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@edgedev/create-edge-app",
3
- "version": "1.1.18",
3
+ "version": "1.1.21",
4
4
  "description": "Create Edge Starter App",
5
5
  "bin": {
6
6
  "create-edge-app": "./bin/cli.js"
@@ -22,6 +22,7 @@
22
22
  "@capacitor/push-notifications": "5.1.0",
23
23
  "@chenfengyuan/vue-number-input": "2",
24
24
  "@edgedev/firebase": "latest",
25
+ "@edgedev/template-engine": "^0.1.12",
25
26
  "@guolao/vue-monaco-editor": "^1.5.5",
26
27
  "@tiptap/extension-image": "^2.11.5",
27
28
  "@tiptap/extension-text-style": "^2.11.5",
@@ -0,0 +1,38 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+ const state = reactive({
4
+ mounted: false,
5
+ head: null,
6
+ })
7
+ definePageMeta({
8
+ middleware: 'auth',
9
+ })
10
+
11
+ const blockId = computed(() => {
12
+ if (route.params.block) {
13
+ return route.params.block
14
+ }
15
+ return ''
16
+ })
17
+
18
+ onMounted(() => {
19
+ state.mounted = true
20
+ })
21
+
22
+ useHead(() => (state.head || {}))
23
+
24
+ const setHead = (newHead) => {
25
+ state.head = newHead
26
+ }
27
+ </script>
28
+
29
+ <template>
30
+ <div
31
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
32
+ >
33
+ <edge-cms-block-editor
34
+ :block-id="blockId"
35
+ @head="setHead"
36
+ />
37
+ </div>
38
+ </template>
@@ -0,0 +1,347 @@
1
+ <script setup>
2
+ const edgeFirebase = inject('edgeFirebase')
3
+ const state = reactive({
4
+ filter: '',
5
+ mounted: false,
6
+ picksFilter: [],
7
+ themesFilter: [],
8
+ })
9
+
10
+ const previewState = reactive({
11
+ loaded: [],
12
+ })
13
+
14
+ definePageMeta({
15
+ middleware: 'auth',
16
+ })
17
+
18
+ const rawInitBlockFiles = import.meta.glob('../../../../edge/components/cms/init_blocks/*.html', {
19
+ as: 'raw',
20
+ eager: true,
21
+ })
22
+
23
+ const INITIAL_BLOCKS = Object.entries(rawInitBlockFiles).map(([path, content]) => {
24
+ const fileName = path.split('/').pop() || ''
25
+ const baseName = fileName.replace(/\.html$/i, '')
26
+ const formattedName = baseName
27
+ .split('_')
28
+ .filter(Boolean)
29
+ .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
30
+ .join(' ')
31
+ return {
32
+ docId: baseName,
33
+ name: formattedName,
34
+ content,
35
+ }
36
+ })
37
+
38
+ const router = useRouter()
39
+
40
+ const seedInitialBlocks = async () => {
41
+ console.log('Seeding initial blocks...')
42
+ console.log(`Found ${INITIAL_BLOCKS.length} initial blocks to seed.`)
43
+ if (!INITIAL_BLOCKS.length)
44
+ return 0
45
+
46
+ const organizationPath = edgeGlobal.edgeState.organizationDocPath
47
+ if (!organizationPath)
48
+ return 0
49
+
50
+ const collectionPath = `${organizationPath}/blocks`
51
+ let created = 0
52
+
53
+ for (const block of INITIAL_BLOCKS) {
54
+ if (!block.docId)
55
+ continue
56
+ try {
57
+ await edgeFirebase.storeDoc(collectionPath, {
58
+ docId: block.docId,
59
+ name: block.name,
60
+ content: block.content,
61
+ tags: [],
62
+ themes: [],
63
+ synced: false,
64
+ version: 1,
65
+ })
66
+ created++
67
+ console.log(`Seeded block "${block.docId}"`)
68
+ }
69
+ catch (error) {
70
+ console.error(`Failed to seed block "${block.docId}"`, error)
71
+ }
72
+ }
73
+
74
+ return created
75
+ }
76
+
77
+ onBeforeMount(async () => {
78
+ restoreFilters()
79
+ if (!edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]) {
80
+ await edgeFirebase.startSnapshot(`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`)
81
+ }
82
+ state.mounted = true
83
+ })
84
+
85
+ const getThemeFromId = (themeId) => {
86
+ const theme = edgeFirebase.data[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`]?.[themeId]
87
+ console.log('getThemeFromId', themeId, theme.name)
88
+ return theme?.name || 'Unknown'
89
+ }
90
+
91
+ const loadingRender = (content) => {
92
+ const safeContent = typeof content === 'string' ? content : ''
93
+ return safeContent.replaceAll('{{loading}}', '').replaceAll('{{loaded}}', 'hidden')
94
+ }
95
+
96
+ const markPreviewLoaded = (isLoading, id) => {
97
+ if (!isLoading && !previewState.loaded.includes(id))
98
+ previewState.loaded.push(id)
99
+ }
100
+
101
+ const hasPreviewLoaded = id => previewState.loaded.includes(id)
102
+
103
+ const FILTER_STORAGE_KEY = 'edge.blocks.filters'
104
+
105
+ const restoreFilters = () => {
106
+ if (typeof localStorage === 'undefined')
107
+ return
108
+ try {
109
+ const raw = localStorage.getItem(FILTER_STORAGE_KEY)
110
+ if (!raw)
111
+ return
112
+ const parsed = JSON.parse(raw)
113
+ state.filter = parsed.filter ?? ''
114
+ state.picksFilter = Array.isArray(parsed.picksFilter) ? parsed.picksFilter : []
115
+ state.themesFilter = Array.isArray(parsed.themesFilter) ? parsed.themesFilter : []
116
+ }
117
+ catch (err) {
118
+ console.warn('Failed to restore block filters', err)
119
+ }
120
+ }
121
+
122
+ const persistFilters = () => {
123
+ if (typeof localStorage === 'undefined')
124
+ return
125
+ const payload = {
126
+ filter: state.filter,
127
+ picksFilter: state.picksFilter,
128
+ themesFilter: state.themesFilter,
129
+ }
130
+ localStorage.setItem(FILTER_STORAGE_KEY, JSON.stringify(payload))
131
+ }
132
+
133
+ watch(
134
+ () => [state.filter, state.picksFilter, state.themesFilter],
135
+ persistFilters,
136
+ { deep: true },
137
+ )
138
+
139
+ const tagOptions = computed(() => {
140
+ const tagsSet = new Set()
141
+ const blocks = edgeFirebase.data?.[`${edgeGlobal.edgeState.organizationDocPath}/blocks`] || {}
142
+ Object.values(blocks).forEach((block) => {
143
+ if (Array.isArray(block.tags))
144
+ block.tags.forEach(tag => tagsSet.add(tag))
145
+ })
146
+ return Array.from(tagsSet).sort((a, b) => a.localeCompare(b)).map(tag => ({ name: tag, title: tag }))
147
+ })
148
+
149
+ const themeOptions = computed(() => {
150
+ const themes = edgeFirebase.data?.[`organizations/${edgeGlobal.edgeState.currentOrganization}/themes`] || {}
151
+ return Object.entries(themes)
152
+ .map(([id, theme]) => ({ name: id, title: theme.name || id }))
153
+ .sort((a, b) => a.title.localeCompare(b.title))
154
+ })
155
+
156
+ const listFilters = computed(() => {
157
+ const filters = []
158
+ if (state.picksFilter.length)
159
+ filters.push({ filterFields: ['tags'], value: state.picksFilter })
160
+ if (state.themesFilter.length)
161
+ filters.push({ filterFields: ['themes'], value: state.themesFilter })
162
+ return filters
163
+ })
164
+ </script>
165
+
166
+ <template>
167
+ <div
168
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
169
+ >
170
+ <edge-dashboard
171
+ :filter="state.filter"
172
+ :filters="listFilters"
173
+ collection="blocks"
174
+ class="pt-0 flex-1"
175
+ >
176
+ <template #header-start="slotProps">
177
+ <component :is="slotProps.icon" class="mr-2" />
178
+ Blocks
179
+ <edge-shad-button
180
+ v-if="slotProps.recordCount === 0"
181
+ variant="outline"
182
+ class="ml-4 h-8 text-xs"
183
+ @click="seedInitialBlocks"
184
+ >
185
+ Seed Blocks
186
+ </edge-shad-button>
187
+ </template>
188
+ <template #header-center>
189
+ <edge-shad-form class="w-full">
190
+ <div class="w-full px-4 md:px-6 pb-2 flex flex-col gap-3 md:flex-row md:items-center">
191
+ <div class="grow">
192
+ <edge-shad-input
193
+ v-model="state.filter"
194
+ name="filter"
195
+ placeholder="Search blocks..."
196
+ class="w-full"
197
+ />
198
+ </div>
199
+ <div>
200
+ <edge-shad-select-tags
201
+ v-model="state.picksFilter"
202
+ :items="tagOptions"
203
+ name="tags"
204
+ placeholder="Filter tags"
205
+ />
206
+ </div>
207
+ <div>
208
+ <edge-shad-select-tags
209
+ v-model="state.themesFilter"
210
+ :items="themeOptions"
211
+ name="themes"
212
+ placeholder="Filter themes"
213
+ />
214
+ </div>
215
+ </div>
216
+ </edge-shad-form>
217
+ </template>
218
+ <template #list="slotProps">
219
+ <div
220
+ class="grid gap-4 pt-4 w-full"
221
+ style="grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));"
222
+ >
223
+ <div
224
+ v-for="item in slotProps.filtered"
225
+ :key="item.docId"
226
+ role="button"
227
+ tabindex="0"
228
+ class="w-full h-full"
229
+ @click="router.push(`/app/dashboard/blocks/${item.docId}`)"
230
+ @keyup.enter="router.push(`/app/dashboard/blocks/${item.docId}`)"
231
+ >
232
+ <Card class="h-full cursor-pointer border border-white/5 bg-gradient-to-br from-slate-950/85 via-slate-950/65 to-slate-900/60 hover:border-primary/50 hover:shadow-[0_22px_55px_-24px_rgba(0,0,0,0.7)] transition">
233
+ <CardContent class="flex flex-col gap-1 p-4 sm:p-5">
234
+ <div class="flex items-start justify-between gap-3">
235
+ <p class="text-lg font-semibold leading-snug line-clamp-2 text-white">
236
+ {{ item.name }}
237
+ </p>
238
+ <edge-shad-button
239
+ size="icon"
240
+ variant="ghost"
241
+ class="h-8 w-8 text-white/80 hover:text-white hover:bg-white/10"
242
+ @click.stop="slotProps.deleteItem(item.docId)"
243
+ >
244
+ <Trash class="h-4 w-4" />
245
+ </edge-shad-button>
246
+ </div>
247
+ <div v-if="item.content" class="block-preview">
248
+ <div class="scale-wrapper">
249
+ <div class="scale-inner scale p-4">
250
+ <edge-cms-block-render
251
+ :content="loadingRender(item.content)"
252
+ :values="item.values"
253
+ :meta="item.meta"
254
+ />
255
+ </div>
256
+ </div>
257
+ <div class="preview-overlay" />
258
+ </div>
259
+ <div v-else class="block-preview-empty">
260
+ Preview unavailable for this block.
261
+ </div>
262
+ <div class="flex flex-wrap items-center gap-1 text-[11px] text-slate-300 uppercase tracking-wide overflow-hidden">
263
+ <edge-chip
264
+ v-for="tag in item.tags?.slice(0, 3) ?? []"
265
+ :key="tag"
266
+ class="bg-primary/40 text-white px-2 py-0.5 text-[10px]"
267
+ >
268
+ {{ tag }}
269
+ </edge-chip>
270
+ <span v-if="item.tags?.length > 3" class="text-white/60">+{{ item.tags.length - 3 }}</span>
271
+ <edge-chip
272
+ v-for="theme in item.themes?.slice(0, 2) ?? []"
273
+ :key="theme"
274
+ class="bg-slate-800 text-white px-2 py-0.5 text-[10px]"
275
+ >
276
+ {{ getThemeFromId(theme) }}
277
+ </edge-chip>
278
+ <span v-if="item.themes?.length > 2" class="text-white/60">+{{ item.themes.length - 2 }}</span>
279
+ <span
280
+ v-if="!(item.tags?.length) && !(item.themes?.length)"
281
+ class="text-slate-500 lowercase"
282
+ >
283
+ none
284
+ </span>
285
+ </div>
286
+ </CardContent>
287
+ </Card>
288
+ </div>
289
+ </div>
290
+ </template>
291
+ </edge-dashboard>
292
+ </div>
293
+ </template>
294
+
295
+ <style scoped>
296
+ .block-preview {
297
+ position: relative;
298
+ height: 220px;
299
+ border-radius: 14px;
300
+ border: 1px solid rgba(255, 255, 255, 0.06);
301
+ background:
302
+ radial-gradient(140% 120% at 15% 15%, rgba(96, 165, 250, 0.08), transparent),
303
+ radial-gradient(120% 120% at 85% 0%, rgba(168, 85, 247, 0.07), transparent),
304
+ linear-gradient(145deg, rgba(10, 14, 26, 0.95), rgba(17, 24, 39, 0.7));
305
+ overflow: hidden;
306
+ box-shadow:
307
+ inset 0 1px 0 rgba(255, 255, 255, 0.02),
308
+ 0 18px 38px rgba(0, 0, 0, 0.35);
309
+ }
310
+
311
+ .block-preview-empty {
312
+ height: 220px;
313
+ border-radius: 14px;
314
+ border: 1px dashed rgba(255, 255, 255, 0.08);
315
+ background: linear-gradient(135deg, rgba(10, 14, 26, 0.65), rgba(17, 24, 39, 0.5));
316
+ color: rgba(255, 255, 255, 0.6);
317
+ display: grid;
318
+ place-items: center;
319
+ font-size: 13px;
320
+ letter-spacing: 0.01em;
321
+ }
322
+
323
+ .preview-overlay {
324
+ pointer-events: none;
325
+ position: absolute;
326
+ inset: 0;
327
+ background: linear-gradient(180deg, rgba(15, 23, 42, 0) 20%, rgba(15, 23, 42, 0.35) 100%);
328
+ }
329
+
330
+ .scale-wrapper {
331
+ width: 100%;
332
+ height: 100%;
333
+ overflow: hidden;
334
+ position: relative;
335
+ }
336
+
337
+ .scale-inner {
338
+ transform-origin: top left;
339
+ display: inline-block;
340
+ min-width: 100%;
341
+ }
342
+
343
+ .scale {
344
+ transform: scale(0.25);
345
+ width: 400%;
346
+ }
347
+ </style>
@@ -0,0 +1,24 @@
1
+ <script setup>
2
+ const state = reactive({
3
+ mounted: false,
4
+ })
5
+ definePageMeta({
6
+ middleware: 'auth',
7
+ })
8
+
9
+ onMounted(() => {
10
+ state.mounted = true
11
+ })
12
+ </script>
13
+
14
+ <template>
15
+ <div
16
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
17
+ >
18
+ <edge-cms-media-manager />
19
+ </div>
20
+ </template>
21
+
22
+ <style>
23
+
24
+ </style>
@@ -0,0 +1,50 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+
4
+ // const edgeGlobal = inject('edgeGlobal')
5
+
6
+ const state = reactive({
7
+ mounted: false,
8
+ head: null,
9
+ })
10
+
11
+ const page = computed(() => {
12
+ if (route.params?.page) {
13
+ return route.params.page
14
+ }
15
+ return ''
16
+ })
17
+
18
+ const site = computed(() => {
19
+ if (route.params?.site) {
20
+ return route.params.site
21
+ }
22
+ return ''
23
+ })
24
+
25
+ definePageMeta({
26
+ middleware: 'auth',
27
+ })
28
+
29
+ onMounted(() => {
30
+ state.mounted = true
31
+ })
32
+
33
+ useHead(() => (state.head || {}))
34
+
35
+ const setHead = (newHead) => {
36
+ state.head = newHead
37
+ }
38
+ </script>
39
+
40
+ <template>
41
+ <div
42
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
43
+ >
44
+ <edge-cms-page
45
+ :site="site"
46
+ :page="page"
47
+ @head="setHead"
48
+ />
49
+ </div>
50
+ </template>
@@ -0,0 +1,42 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+
4
+ // const edgeGlobal = inject('edgeGlobal')
5
+
6
+ const state = reactive({
7
+ mounted: false,
8
+ })
9
+
10
+ const page = computed(() => {
11
+ if (route.params?.page) {
12
+ return route.params.page
13
+ }
14
+ return ''
15
+ })
16
+
17
+ const site = computed(() => {
18
+ if (route.params?.site) {
19
+ return route.params.site
20
+ }
21
+ return ''
22
+ })
23
+
24
+ definePageMeta({
25
+ middleware: 'auth',
26
+ })
27
+
28
+ onMounted(() => {
29
+ state.mounted = true
30
+ })
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
36
+ >
37
+ <edge-cms-site
38
+ :site="site"
39
+ :page="page"
40
+ />
41
+ </div>
42
+ </template>
@@ -0,0 +1,114 @@
1
+ <script setup>
2
+ import { toTypedSchema } from '@vee-validate/zod'
3
+ import * as z from 'zod'
4
+ import { Loader2 } from 'lucide-vue-next'
5
+ const route = useRoute()
6
+ // const edgeGlobal = inject('edgeGlobal')
7
+
8
+ const edgeFirebase = inject('edgeFirebase')
9
+ const isAiBusy = status => status === 'queued' || status === 'running'
10
+
11
+ const state = reactive({
12
+ filter: '',
13
+ newDocs: {
14
+ sites: {
15
+ name: { bindings: { 'field-type': 'text', 'label': 'Name', 'helper': 'Name' }, cols: '12', value: '' },
16
+ },
17
+ },
18
+ })
19
+
20
+ const schemas = {
21
+ sites: toTypedSchema(z.object({
22
+ name: z.string({
23
+ required_error: 'Name is required',
24
+ }).min(1, { message: 'Name is required' }),
25
+ })),
26
+ }
27
+
28
+ const collection = computed(() => {
29
+ if (route.params.collection) {
30
+ return route.params.collection
31
+ }
32
+ return ''
33
+ })
34
+ const docId = computed(() => {
35
+ if (route.params.docId) {
36
+ return route.params.docId
37
+ }
38
+ return ''
39
+ })
40
+ definePageMeta({
41
+ middleware: 'auth',
42
+ })
43
+
44
+ onBeforeMount(() => {
45
+ // edgeGlobal.showLeftPanel(true)
46
+ })
47
+ const isAdmin = computed(() => {
48
+ return edgeGlobal.isAdminGlobal(edgeFirebase).value
49
+ })
50
+ const queryField = computed(() => {
51
+ if (!isAdmin.value) {
52
+ return 'users'
53
+ }
54
+ return ''
55
+ })
56
+
57
+ const queryValue = computed(() => {
58
+ if (!isAdmin.value) {
59
+ return [edgeFirebase?.user?.uid]
60
+ }
61
+ return ''
62
+ })
63
+
64
+ const queryOperator = computed(() => {
65
+ if (!isAdmin.value) {
66
+ return 'array-contains-any'
67
+ }
68
+ return ''
69
+ })
70
+ </script>
71
+
72
+ <template>
73
+ <div
74
+ v-if="edgeGlobal.edgeState.organizationDocPath"
75
+ >
76
+ <edge-dashboard :load-first-if-one="!isAdmin" :filter="state.filter" :query-field="queryField" :query-value="queryValue" :query-operator="queryOperator" collection="sites" class="flex-1 pt-0">
77
+ <template #list="slotProps">
78
+ <template v-for="item in slotProps.filtered" :key="item.docId">
79
+ <edge-shad-button
80
+ variant="text"
81
+ class="cursor-pointer w-full flex justify-between items-center py-2 gap-3"
82
+ :to="isAiBusy(item.aiBootstrapStatus) ? undefined : `/app/dashboard/sites/${item.docId}`"
83
+ :disabled="isAiBusy(item.aiBootstrapStatus)"
84
+ >
85
+ <div>
86
+ <Avatar class="cursor-pointer p-0 h-8 w-8 mr-2">
87
+ <FilePenLine class="h-5 w-5" />
88
+ </Avatar>
89
+ </div>
90
+ <div class="grow text-left">
91
+ <div class="text-lg">
92
+ {{ item.name }}
93
+ </div>
94
+ <div v-if="isAiBusy(item.aiBootstrapStatus)" class="flex items-center gap-2 text-xs text-muted-foreground">
95
+ <Loader2 class="h-3 w-3 animate-spin" />
96
+ <span>AI is preparing this site</span>
97
+ </div>
98
+ </div>
99
+ <div>
100
+ <edge-shad-button
101
+ size="icon"
102
+ class="bg-slate-600 h-7 w-7"
103
+ @click.stop="slotProps.deleteItem(item.docId)"
104
+ >
105
+ <Trash class="h-5 w-5" />
106
+ </edge-shad-button>
107
+ </div>
108
+ </edge-shad-button>
109
+ <Separator class="dark:bg-slate-600" />
110
+ </template>
111
+ </template>
112
+ </edge-dashboard>
113
+ </div>
114
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+
4
+ const state = reactive({
5
+ mounted: false,
6
+ head: null,
7
+ })
8
+
9
+ const page = computed(() => {
10
+ if (route.params?.page) {
11
+ return route.params.page
12
+ }
13
+ return ''
14
+ })
15
+
16
+ const site = computed(() => 'templates')
17
+
18
+ definePageMeta({
19
+ middleware: 'auth',
20
+ })
21
+
22
+ onMounted(() => {
23
+ state.mounted = true
24
+ })
25
+
26
+ useHead(() => (state.head || {}))
27
+
28
+ const setHead = (newHead) => {
29
+ state.head = newHead
30
+ }
31
+ </script>
32
+
33
+ <template>
34
+ <div
35
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
36
+ >
37
+ <edge-cms-page
38
+ :site="site"
39
+ :page="page"
40
+ :is-template-site="true"
41
+ @head="setHead"
42
+ />
43
+ </div>
44
+ </template>
@@ -0,0 +1,35 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+
4
+ // const edgeGlobal = inject('edgeGlobal')
5
+
6
+ const state = reactive({
7
+ mounted: false,
8
+ })
9
+
10
+ const page = computed(() => {
11
+ if (route.params?.page) {
12
+ return route.params.page
13
+ }
14
+ return ''
15
+ })
16
+
17
+ definePageMeta({
18
+ middleware: 'auth',
19
+ })
20
+
21
+ onMounted(() => {
22
+ state.mounted = true
23
+ })
24
+ </script>
25
+
26
+ <template>
27
+ <div
28
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
29
+ >
30
+ <edge-cms-site
31
+ site="templates"
32
+ :page="page"
33
+ />
34
+ </div>
35
+ </template>
@@ -0,0 +1,37 @@
1
+ <script setup>
2
+ const route = useRoute()
3
+ const state = reactive({
4
+ mounted: false,
5
+ })
6
+ definePageMeta({
7
+ middleware: 'auth',
8
+ head: null,
9
+ })
10
+
11
+ const themeId = computed(() => {
12
+ if (route.params.theme) {
13
+ return route.params.theme
14
+ }
15
+ return ''
16
+ })
17
+
18
+ useHead(() => (state.head || {}))
19
+
20
+ onMounted(() => {
21
+ state.mounted = true
22
+ })
23
+ const setHead = (newHead) => {
24
+ state.head = newHead
25
+ }
26
+ </script>
27
+
28
+ <template>
29
+ <div
30
+ v-if="edgeGlobal.edgeState.organizationDocPath && state.mounted"
31
+ >
32
+ <edge-cms-theme-editor
33
+ :theme-id="themeId"
34
+ @head="setHead"
35
+ />
36
+ </div>
37
+ </template>
@@ -0,0 +1,44 @@
1
+ <script setup>
2
+ const state = reactive({
3
+ filter: '',
4
+ })
5
+
6
+ definePageMeta({
7
+ middleware: 'auth',
8
+ })
9
+ </script>
10
+
11
+ <template>
12
+ <div
13
+ v-if="edgeGlobal.edgeState.organizationDocPath"
14
+ >
15
+ <edge-dashboard :filter="state.filter" collection="themes" class="pt-0 flex-1">
16
+ <template #list="slotProps">
17
+ <template v-for="item in slotProps.filtered" :key="item.docId">
18
+ <edge-shad-button variant="text" class="cursor-pointer w-full flex justify-between items-center py-2 gap-3" :to="`/app/dashboard/themes/${item.docId}`">
19
+ <div>
20
+ <Avatar class="cursor-pointer p-0 h-8 w-8 mr-2">
21
+ <FilePenLine class="h-5 w-5" />
22
+ </Avatar>
23
+ </div>
24
+ <div class="grow text-left">
25
+ <div class="text-lg">
26
+ {{ item.name }}
27
+ </div>
28
+ </div>
29
+ <div>
30
+ <edge-shad-button
31
+ size="icon"
32
+ class="bg-slate-600 h-7 w-7"
33
+ @click.stop="slotProps.deleteItem(item.docId)"
34
+ >
35
+ <Trash class="h-5 w-5" />
36
+ </edge-shad-button>
37
+ </div>
38
+ </edge-shad-button>
39
+ <Separator class="dark:bg-slate-600" />
40
+ </template>
41
+ </template>
42
+ </edge-dashboard>
43
+ </div>
44
+ </template>
package/pages/app.vue CHANGED
@@ -37,6 +37,44 @@ const menuBuilder = () => {
37
37
  icon: 'Package',
38
38
  devOnly: true,
39
39
  },
40
+ {
41
+ title: 'Sites',
42
+ to: '/app/dashboard/sites',
43
+ icon: 'LayoutPanelTop',
44
+ devOnly: true,
45
+ submenu: [
46
+ {
47
+ title: 'Sites',
48
+ to: '/app/dashboard/sites',
49
+ icon: 'LayoutPanelTop',
50
+ devOnly: true,
51
+ },
52
+ {
53
+ title: 'Media',
54
+ to: '/app/dashboard/media',
55
+ icon: 'Image',
56
+ devOnly: true,
57
+ },
58
+ {
59
+ title: 'Blocks',
60
+ to: '/app/dashboard/blocks',
61
+ icon: 'Blocks',
62
+ devOnly: true,
63
+ },
64
+ {
65
+ title: 'Templates',
66
+ to: '/app/dashboard/templates',
67
+ icon: 'LayoutList',
68
+ devOnly: true,
69
+ },
70
+ {
71
+ title: 'Themes',
72
+ to: '/app/dashboard/themes',
73
+ icon: 'Paintbrush',
74
+ devOnly: true,
75
+ },
76
+ ],
77
+ },
40
78
  {
41
79
  title: 'Settings',
42
80
  to: '/app/account/my-profile',
package/plugins/icons.ts CHANGED
@@ -1,11 +1,19 @@
1
1
  import {
2
2
  AlertCircle,
3
+ AlignHorizontalJustifyStart,
3
4
  ArrowLeft,
4
5
  ArrowRight,
6
+ Award,
7
+ BadgeCheck,
8
+ Blocks,
9
+ Bold,
5
10
  BookImage,
11
+ BookOpen,
12
+ Bot,
6
13
  Box,
7
14
  Braces,
8
15
  Brackets,
16
+ Building,
9
17
  CalendarIcon,
10
18
  Check,
11
19
  ChevronDown,
@@ -16,34 +24,72 @@ import {
16
24
  ChevronUp,
17
25
  ChevronsUpDown,
18
26
  CircleUser,
27
+ CircleX,
28
+ CloudUpload,
29
+ Code,
19
30
  Copy,
31
+ Download,
20
32
  Eye,
21
33
  EyeOff,
34
+ File,
35
+ FileCode2,
36
+ FileImage,
22
37
  FilePenLine,
38
+ FilePlus2,
39
+ FileSpreadsheet,
40
+ FileText,
41
+ FolderOpen,
42
+ FolderTree,
23
43
  Fullscreen,
44
+ Globe,
24
45
  Grip,
25
46
  Group,
47
+ Heading1,
48
+ Heading2,
49
+ Heading3,
50
+ Heading4,
26
51
  Hourglass,
27
52
  Image,
28
- Info, LayoutDashboard,
53
+ Inbox,
54
+ Info,
55
+ Italic,
56
+ Layers3,
57
+ LayoutDashboard,
58
+ LayoutList,
59
+ LayoutPanelTop,
60
+ Link,
29
61
  List,
62
+ ListOrdered,
30
63
  ListPlus,
31
64
  Loader2,
32
65
  LogOut,
66
+ Menu,
33
67
  MenuSquare,
34
68
  MoreHorizontal,
35
69
  Newspaper,
36
70
  Package,
71
+ Paintbrush,
37
72
  PauseCircle,
38
73
  Pencil,
39
74
  PlusIcon,
75
+ Printer,
76
+ Save,
40
77
  Settings,
41
78
  Settings2,
79
+ SquareCode,
80
+ StickyNote,
81
+ Store,
82
+ Strikethrough,
83
+ TextQuote,
42
84
  Trash,
43
85
  TrashIcon,
86
+ TriangleAlert,
87
+ Underline,
44
88
  Upload,
45
89
  User,
90
+ UserCheck,
46
91
  Users,
92
+ View,
47
93
  X,
48
94
  ZoomIn,
49
95
  } from 'lucide-vue-next'
@@ -97,4 +143,49 @@ export default defineNuxtPlugin((nuxtApp) => {
97
143
  nuxtApp.vueApp.component('ChevronRightCircle', ChevronRightCircle)
98
144
  nuxtApp.vueApp.component('Fullscreen', Fullscreen)
99
145
  nuxtApp.vueApp.component('MenuSquare', MenuSquare)
146
+ nuxtApp.vueApp.component('Blocks', Blocks)
147
+ nuxtApp.vueApp.component('LayoutPanelTop', LayoutPanelTop)
148
+ nuxtApp.vueApp.component('Building', Building)
149
+ nuxtApp.vueApp.component('Store', Store)
150
+ nuxtApp.vueApp.component('UserCheck', UserCheck)
151
+ nuxtApp.vueApp.component('Award', Award)
152
+ nuxtApp.vueApp.component('BadgeCheck', BadgeCheck)
153
+ nuxtApp.vueApp.component('BookOpen', BookOpen)
154
+ nuxtApp.vueApp.component('AlignHorizontalJustifyStart', AlignHorizontalJustifyStart)
155
+ nuxtApp.vueApp.component('Bold', Bold)
156
+ nuxtApp.vueApp.component('Bot', Bot)
157
+ nuxtApp.vueApp.component('CircleX', CircleX)
158
+ nuxtApp.vueApp.component('CloudUpload', CloudUpload)
159
+ nuxtApp.vueApp.component('Code', Code)
160
+ nuxtApp.vueApp.component('Download', Download)
161
+ nuxtApp.vueApp.component('File', File)
162
+ nuxtApp.vueApp.component('FileCode2', FileCode2)
163
+ nuxtApp.vueApp.component('FileImage', FileImage)
164
+ nuxtApp.vueApp.component('FilePlus2', FilePlus2)
165
+ nuxtApp.vueApp.component('FileSpreadsheet', FileSpreadsheet)
166
+ nuxtApp.vueApp.component('FileText', FileText)
167
+ nuxtApp.vueApp.component('FolderOpen', FolderOpen)
168
+ nuxtApp.vueApp.component('FolderTree', FolderTree)
169
+ nuxtApp.vueApp.component('Globe', Globe)
170
+ nuxtApp.vueApp.component('Heading1', Heading1)
171
+ nuxtApp.vueApp.component('Heading2', Heading2)
172
+ nuxtApp.vueApp.component('Heading3', Heading3)
173
+ nuxtApp.vueApp.component('Heading4', Heading4)
174
+ nuxtApp.vueApp.component('Inbox', Inbox)
175
+ nuxtApp.vueApp.component('Italic', Italic)
176
+ nuxtApp.vueApp.component('Layers3', Layers3)
177
+ nuxtApp.vueApp.component('LayoutList', LayoutList)
178
+ nuxtApp.vueApp.component('Link', Link)
179
+ nuxtApp.vueApp.component('ListOrdered', ListOrdered)
180
+ nuxtApp.vueApp.component('Menu', Menu)
181
+ nuxtApp.vueApp.component('Printer', Printer)
182
+ nuxtApp.vueApp.component('SquareCode', SquareCode)
183
+ nuxtApp.vueApp.component('StickyNote', StickyNote)
184
+ nuxtApp.vueApp.component('Strikethrough', Strikethrough)
185
+ nuxtApp.vueApp.component('TextQuote', TextQuote)
186
+ nuxtApp.vueApp.component('TriangleAlert', TriangleAlert)
187
+ nuxtApp.vueApp.component('Underline', Underline)
188
+ nuxtApp.vueApp.component('View', View)
189
+ nuxtApp.vueApp.component('Save', Save)
190
+ nuxtApp.vueApp.component('Paintbrush', Paintbrush)
100
191
  })
@@ -0,0 +1,36 @@
1
+ import { install as VueMonacoEditorPlugin, loader } from '@guolao/vue-monaco-editor'
2
+ import * as monaco from 'monaco-editor'
3
+ import EditorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'
4
+ import JsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'
5
+ import CssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'
6
+ import HtmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'
7
+ import TsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'
8
+ import { defineNuxtPlugin } from '#app'
9
+
10
+ self.MonacoEnvironment = {
11
+ getWorker(_, label) {
12
+ if (label === 'json') {
13
+ return new JsonWorker()
14
+ }
15
+ if (label === 'css' || label === 'scss' || label === 'less') {
16
+ return new CssWorker()
17
+ }
18
+ if (label === 'html' || label === 'handlebars' || label === 'razor') {
19
+ return new HtmlWorker()
20
+ }
21
+ if (label === 'typescript' || label === 'javascript') {
22
+ return new TsWorker()
23
+ }
24
+ return new EditorWorker()
25
+ },
26
+ }
27
+
28
+ loader.config({ monaco })
29
+
30
+ export default defineNuxtPlugin((nuxtApp) => {
31
+ nuxtApp.vueApp.use(VueMonacoEditorPlugin, {
32
+ paths: {
33
+ vs: 'https://cdn.jsdelivr.net/npm/monaco-editor@0.43.0/min/vs',
34
+ },
35
+ })
36
+ })