@live-change/frontend-template 0.9.199 → 0.9.200
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/.claude/rules/live-change-backend-actions-views-triggers.md +62 -0
- package/.claude/rules/live-change-backend-event-sourcing.md +186 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +72 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +26 -0
- package/.claude/settings.json +32 -0
- package/.claude/skills/create-skills-and-rules/SKILL.md +248 -0
- package/.claude/skills/live-change-backend-change-triggers/SKILL.md +186 -0
- package/.claude/skills/live-change-design-actions-views-triggers/SKILL.md +462 -0
- package/.claude/skills/live-change-design-models-relations/SKILL.md +230 -0
- package/.claude/skills/live-change-design-service/SKILL.md +133 -0
- package/.claude/skills/live-change-frontend-accessible-objects/SKILL.md +384 -0
- package/.claude/skills/live-change-frontend-accessible-objects.md +383 -0
- package/.claude/skills/live-change-frontend-action-buttons/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-action-form/SKILL.md +149 -0
- package/.claude/skills/live-change-frontend-analytics/SKILL.md +147 -0
- package/.claude/skills/live-change-frontend-command-forms/SKILL.md +216 -0
- package/.claude/skills/live-change-frontend-data-views/SKILL.md +183 -0
- package/.claude/skills/live-change-frontend-editor-form/SKILL.md +240 -0
- package/.claude/skills/live-change-frontend-locale-time/SKILL.md +172 -0
- package/.claude/skills/live-change-frontend-page-list-detail/SKILL.md +201 -0
- package/.claude/skills/live-change-frontend-range-list/SKILL.md +129 -0
- package/.claude/skills/live-change-frontend-ssr-setup/SKILL.md +119 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +88 -0
- package/.cursor/rules/live-change-backend-event-sourcing.mdc +185 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +62 -0
- package/.cursor/skills/create-skills-and-rules.md +248 -0
- package/.cursor/skills/live-change-backend-change-triggers.md +186 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +178 -79
- package/.cursor/skills/live-change-design-models-relations.md +112 -50
- package/.cursor/skills/live-change-design-service.md +1 -0
- package/.cursor/skills/live-change-frontend-accessible-objects.md +384 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +1 -0
- package/.cursor/skills/live-change-frontend-action-form.md +9 -3
- package/.cursor/skills/live-change-frontend-analytics.md +1 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +1 -0
- package/.cursor/skills/live-change-frontend-data-views.md +1 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +135 -72
- package/.cursor/skills/live-change-frontend-locale-time.md +1 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +1 -0
- package/.cursor/skills/live-change-frontend-range-list.md +1 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +1 -0
- package/front/src/router.js +2 -1
- package/opencode.json +10 -0
- package/package.json +52 -50
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-analytics
|
|
3
|
+
description: Integrate analytics tracking with analytics.emit and provider wiring
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-analytics (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when you add **analytics tracking** to a LiveChange frontend.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You need to track user actions, page views, or custom events.
|
|
13
|
+
- You are integrating PostHog, GA4, or another analytics provider.
|
|
14
|
+
- You need consent-aware analytics.
|
|
15
|
+
|
|
16
|
+
## Step 1 – Emit events with `analytics`
|
|
17
|
+
|
|
18
|
+
Import `analytics` from `@live-change/vue3-components` and emit events:
|
|
19
|
+
|
|
20
|
+
```javascript
|
|
21
|
+
import { analytics } from '@live-change/vue3-components'
|
|
22
|
+
|
|
23
|
+
// In a component or handler:
|
|
24
|
+
analytics.emit('article:published', { articleId: article.id })
|
|
25
|
+
analytics.emit('button:clicked', { action: 'delete', target: 'article' })
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Standard built-in events:
|
|
29
|
+
|
|
30
|
+
| Event | Payload | When emitted |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `pageView` | route `to` object | On route change (automatic in App.vue) |
|
|
33
|
+
| `user:identification` | `{ user, session, identification, contacts }` | When user identity is known |
|
|
34
|
+
| `consent` | `{ analytics: boolean }` | When user grants/denies tracking consent |
|
|
35
|
+
| `locale:change` | locale settings object | When language/locale changes |
|
|
36
|
+
|
|
37
|
+
## Step 2 – Wire user identification in App.vue
|
|
38
|
+
|
|
39
|
+
Emit `user:identification` when the user profile is loaded:
|
|
40
|
+
|
|
41
|
+
```javascript
|
|
42
|
+
import { analytics } from '@live-change/vue3-components'
|
|
43
|
+
import { usePath, live, useClient } from '@live-change/vue3-ssr'
|
|
44
|
+
import { computed, watch } from 'vue'
|
|
45
|
+
|
|
46
|
+
const client = useClient()
|
|
47
|
+
const path = usePath()
|
|
48
|
+
|
|
49
|
+
if(typeof window !== 'undefined') {
|
|
50
|
+
Promise.all([
|
|
51
|
+
live(path.userIdentification.myIdentification()),
|
|
52
|
+
]).then(([identification]) => {
|
|
53
|
+
const fullIdentification = computed(() => ({
|
|
54
|
+
user: client.value.user,
|
|
55
|
+
session: client.value.session,
|
|
56
|
+
identification: identification.value,
|
|
57
|
+
}))
|
|
58
|
+
watch(fullIdentification, (newId) => {
|
|
59
|
+
analytics.emit('user:identification', newId)
|
|
60
|
+
}, { immediate: true })
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Step 3 – Create a provider file (e.g. PostHog)
|
|
66
|
+
|
|
67
|
+
Create a file like `src/analytics/posthog.js`:
|
|
68
|
+
|
|
69
|
+
```javascript
|
|
70
|
+
import { analytics } from '@live-change/vue3-components'
|
|
71
|
+
import posthog from 'posthog-js'
|
|
72
|
+
|
|
73
|
+
posthog.init('phc_YOUR_KEY', {
|
|
74
|
+
api_host: 'https://eu.i.posthog.com',
|
|
75
|
+
person_profiles: 'always'
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// Page views
|
|
79
|
+
analytics.on('pageView', (to) => {
|
|
80
|
+
posthog.register({
|
|
81
|
+
route: { name: to.name, params: to.params }
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// User identification
|
|
86
|
+
analytics.on('user:identification', (identification) => {
|
|
87
|
+
if (!identification.user) {
|
|
88
|
+
posthog.reset()
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
posthog.identify(identification.user, {
|
|
92
|
+
firstName: identification.identification?.firstName,
|
|
93
|
+
lastName: identification.identification?.lastName,
|
|
94
|
+
})
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Consent
|
|
98
|
+
analytics.on('consent', (payload) => {
|
|
99
|
+
if (payload?.analytics === true) {
|
|
100
|
+
posthog.opt_in_capturing()
|
|
101
|
+
} else {
|
|
102
|
+
posthog.opt_out_capturing()
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
// Catch-all for custom events
|
|
107
|
+
const ignored = ['user:identification', 'pageView', 'consent']
|
|
108
|
+
analytics.on('*', (type, event) => {
|
|
109
|
+
if (ignored.includes(type)) return
|
|
110
|
+
posthog.capture(type, event)
|
|
111
|
+
})
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Import it in `App.vue`:
|
|
115
|
+
|
|
116
|
+
```javascript
|
|
117
|
+
import './analytics/posthog'
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Step 4 – Emit custom events in components
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
// Track a form submission
|
|
124
|
+
analytics.emit('form:submitted', { formName: 'contactForm' })
|
|
125
|
+
|
|
126
|
+
// Track a feature usage
|
|
127
|
+
analytics.emit('feature:used', { feature: 'darkMode', enabled: true })
|
|
128
|
+
|
|
129
|
+
// Track navigation
|
|
130
|
+
analytics.emit('navigation:click', { target: 'pricing', source: 'navbar' })
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Step 5 – Consent handling
|
|
134
|
+
|
|
135
|
+
Emit consent events from your consent banner:
|
|
136
|
+
|
|
137
|
+
```javascript
|
|
138
|
+
function acceptAnalytics() {
|
|
139
|
+
analytics.emit('consent', { analytics: true })
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function rejectAnalytics() {
|
|
143
|
+
analytics.emit('consent', { analytics: false })
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Provider files listen for `consent` and enable/disable tracking accordingly.
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-command-forms
|
|
3
|
+
description: Build forms with api.command, command-form, workingZone, confirm and toast
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-command-forms (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when you build **forms and actions** for a LiveChange frontend:
|
|
9
|
+
|
|
10
|
+
- calling `api.command`,
|
|
11
|
+
- using `<command-form>`,
|
|
12
|
+
- handling destructive actions with confirm + toast.
|
|
13
|
+
|
|
14
|
+
## Choosing the right pattern
|
|
15
|
+
|
|
16
|
+
Before using this skill, pick the right approach:
|
|
17
|
+
|
|
18
|
+
| Pattern | When to use |
|
|
19
|
+
|---|---|
|
|
20
|
+
| `editorData` | **Editing model records** (create/update). Drafts, validation, `AutoField`. See `live-change-frontend-editor-form` skill. |
|
|
21
|
+
| `actionData` | **One-shot action forms** (not CRUD). Submit once → done. See `live-change-frontend-action-form` skill. |
|
|
22
|
+
| `api.command` | **Single button or programmatic calls** (no form fields). This skill, Step 1. |
|
|
23
|
+
| `<command-form>` | **Avoid.** Legacy. Only for trivial prototypes without drafts or `AutoField`. This skill, Step 2. |
|
|
24
|
+
|
|
25
|
+
**Decision flow:**
|
|
26
|
+
|
|
27
|
+
1. Does the user fill in form fields? → **No**: use `api.command` (this skill).
|
|
28
|
+
2. Is it editing a model record? → **Yes**: use `editorData`.
|
|
29
|
+
3. Is it a one-shot action? → **Yes**: use `actionData`.
|
|
30
|
+
4. Only use `<command-form>` for the simplest throwaway cases.
|
|
31
|
+
|
|
32
|
+
## Step 1 – Use `api.command` directly
|
|
33
|
+
|
|
34
|
+
1. Import and create `api`:
|
|
35
|
+
|
|
36
|
+
```js
|
|
37
|
+
import { api as useApi } from '@live-change/vue3-ssr'
|
|
38
|
+
|
|
39
|
+
const api = useApi()
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. Call commands as:
|
|
43
|
+
|
|
44
|
+
```js
|
|
45
|
+
await api.command(['deviceManager', 'createMyUserDevice'], {
|
|
46
|
+
name: 'My device'
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], {
|
|
50
|
+
device: id
|
|
51
|
+
})
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. Wrap in `try/catch` if you need custom error handling.
|
|
55
|
+
|
|
56
|
+
## Step 2 – `<command-form>` (legacy – prefer editorData/actionData)
|
|
57
|
+
|
|
58
|
+
> **Note:** `<command-form>` is the oldest pattern. For new code, prefer `editorData` (model editing) or `actionData` (one-shot actions) – they support drafts, `AutoField`, and richer state management.
|
|
59
|
+
|
|
60
|
+
1. Use `<command-form>` only for trivial forms without drafts or `AutoField`.
|
|
61
|
+
2. Provide:
|
|
62
|
+
- `service` – service name,
|
|
63
|
+
- `action` – action name,
|
|
64
|
+
- `fields` – constant/hidden fields (e.g. ids).
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
|
|
68
|
+
```vue
|
|
69
|
+
<command-form
|
|
70
|
+
service="deviceManager"
|
|
71
|
+
action="updateMyUserDevice"
|
|
72
|
+
:fields="{ device: deviceId }"
|
|
73
|
+
>
|
|
74
|
+
<template #default="{ fieldProps, submit, busy }">
|
|
75
|
+
<div class="space-y-4">
|
|
76
|
+
<div>
|
|
77
|
+
<label class="block text-sm font-medium mb-1">
|
|
78
|
+
Name
|
|
79
|
+
</label>
|
|
80
|
+
<InputText v-bind="fieldProps('name')" class="w-full" />
|
|
81
|
+
</div>
|
|
82
|
+
<div class="flex justify-end gap-2">
|
|
83
|
+
<Button
|
|
84
|
+
label="Save"
|
|
85
|
+
icon="pi pi-check"
|
|
86
|
+
:loading="busy"
|
|
87
|
+
@click="submit"
|
|
88
|
+
/>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
</template>
|
|
92
|
+
</command-form>
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Step 3 – Confirm + Toast for destructive actions
|
|
96
|
+
|
|
97
|
+
1. Use PrimeVue `useConfirm` and `useToast`.
|
|
98
|
+
2. Put the `api.command` call in `accept`.
|
|
99
|
+
|
|
100
|
+
Example:
|
|
101
|
+
|
|
102
|
+
```js
|
|
103
|
+
import { useConfirm } from 'primevue/useconfirm'
|
|
104
|
+
import { useToast } from 'primevue/usetoast'
|
|
105
|
+
import { api as useApi } from '@live-change/vue3-ssr'
|
|
106
|
+
|
|
107
|
+
const api = useApi()
|
|
108
|
+
const confirm = useConfirm()
|
|
109
|
+
const toast = useToast()
|
|
110
|
+
|
|
111
|
+
function deleteDevice(id) {
|
|
112
|
+
confirm.require({
|
|
113
|
+
message: 'Are you sure you want to delete this device?',
|
|
114
|
+
header: 'Confirmation',
|
|
115
|
+
icon: 'pi pi-exclamation-triangle',
|
|
116
|
+
accept: async () => {
|
|
117
|
+
await api.command(['deviceManager', 'deleteMyUserDevice'], { device: id })
|
|
118
|
+
toast.add({
|
|
119
|
+
severity: 'success',
|
|
120
|
+
summary: 'Deleted',
|
|
121
|
+
life: 2000
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
})
|
|
125
|
+
}
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
## Step 3b – WorkingZone for button actions (outside forms)
|
|
129
|
+
|
|
130
|
+
When a button triggers an async action outside a form, wrap it with `workingZone.addPromise()` so the global loading spinner activates:
|
|
131
|
+
|
|
132
|
+
```js
|
|
133
|
+
import { inject } from 'vue'
|
|
134
|
+
import { useToast } from 'primevue/usetoast'
|
|
135
|
+
import { useActions } from '@live-change/vue3-ssr'
|
|
136
|
+
|
|
137
|
+
const workingZone = inject('workingZone')
|
|
138
|
+
const toast = useToast()
|
|
139
|
+
const actions = useActions()
|
|
140
|
+
|
|
141
|
+
function createDevice() {
|
|
142
|
+
workingZone.addPromise('createDevice', (async () => {
|
|
143
|
+
const result = await actions.deviceManager.createMyUserDevice({ name: 'New device' })
|
|
144
|
+
toast.add({ severity: 'success', summary: 'Created', life: 2000 })
|
|
145
|
+
router.push({ name: 'device', params: { device: result } })
|
|
146
|
+
})())
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Combine with `confirm.require()` for destructive actions:
|
|
151
|
+
|
|
152
|
+
```js
|
|
153
|
+
function deleteDevice(id) {
|
|
154
|
+
confirm.require({
|
|
155
|
+
message: 'Are you sure?',
|
|
156
|
+
header: 'Confirmation',
|
|
157
|
+
icon: 'pi pi-trash',
|
|
158
|
+
acceptClass: 'p-button-danger',
|
|
159
|
+
accept: async () => {
|
|
160
|
+
workingZone.addPromise('deleteDevice', (async () => {
|
|
161
|
+
await actions.deviceManager.deleteMyUserDevice({ device: id })
|
|
162
|
+
toast.add({ severity: 'success', summary: 'Deleted', life: 2000 })
|
|
163
|
+
})())
|
|
164
|
+
}
|
|
165
|
+
})
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
For a detailed guide, see the `live-change-frontend-action-buttons` skill.
|
|
170
|
+
|
|
171
|
+
## Step 4 – Pattern for sensitive values (toggle + copy)
|
|
172
|
+
|
|
173
|
+
1. By default, hide sensitive values (keys, tokens, etc.).
|
|
174
|
+
2. Add:
|
|
175
|
+
- a toggle button (eye / eye-slash),
|
|
176
|
+
- a copy button with a toast.
|
|
177
|
+
|
|
178
|
+
Example:
|
|
179
|
+
|
|
180
|
+
```vue
|
|
181
|
+
<template>
|
|
182
|
+
<div class="flex items-center gap-2">
|
|
183
|
+
<code>
|
|
184
|
+
{{ revealed ? item.pairingKey : '••••••••••••' }}
|
|
185
|
+
</code>
|
|
186
|
+
<Button
|
|
187
|
+
:icon="revealed ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
|
188
|
+
text
|
|
189
|
+
@click="revealed = !revealed"
|
|
190
|
+
/>
|
|
191
|
+
<Button
|
|
192
|
+
icon="pi pi-copy"
|
|
193
|
+
text
|
|
194
|
+
@click="copyToClipboard(item.pairingKey)"
|
|
195
|
+
/>
|
|
196
|
+
</div>
|
|
197
|
+
</template>
|
|
198
|
+
|
|
199
|
+
<script setup>
|
|
200
|
+
import { ref } from 'vue'
|
|
201
|
+
import { useToast } from 'primevue/usetoast'
|
|
202
|
+
|
|
203
|
+
const toast = useToast()
|
|
204
|
+
const revealed = ref(false)
|
|
205
|
+
|
|
206
|
+
async function copyToClipboard(text) {
|
|
207
|
+
await navigator.clipboard.writeText(text)
|
|
208
|
+
toast.add({
|
|
209
|
+
severity: 'info',
|
|
210
|
+
summary: 'Copied',
|
|
211
|
+
life: 1500
|
|
212
|
+
})
|
|
213
|
+
}
|
|
214
|
+
</script>
|
|
215
|
+
```
|
|
216
|
+
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: live-change-frontend-data-views
|
|
3
|
+
description: Build reactive data views with usePath, live, .with() and useClient auth guards
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Skill: live-change-frontend-data-views (Claude Code)
|
|
7
|
+
|
|
8
|
+
Use this skill when you build **reactive data views** using `usePath`, `live`, `.with()`, and `useClient` in a LiveChange frontend.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- You are loading data from backend views.
|
|
13
|
+
- Paths depend on reactive values (route params, props, client state).
|
|
14
|
+
- You need to load related objects alongside the main data.
|
|
15
|
+
- You need to restrict data loading for unauthenticated users.
|
|
16
|
+
|
|
17
|
+
## Step 1 – Basic data loading with computed paths
|
|
18
|
+
|
|
19
|
+
When paths depend on reactive values (route params, props), wrap them in `computed()`:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { computed, unref } from 'vue'
|
|
23
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
24
|
+
import { useRoute } from 'vue-router'
|
|
25
|
+
|
|
26
|
+
const path = usePath()
|
|
27
|
+
const route = useRoute()
|
|
28
|
+
const articleId = route.params.article
|
|
29
|
+
|
|
30
|
+
const articlePath = computed(() => path.blog.article({ article: unref(articleId) }))
|
|
31
|
+
const commentsPath = computed(() => path.blog.articleComments({ article: unref(articleId) }))
|
|
32
|
+
|
|
33
|
+
const [article, comments] = await Promise.all([
|
|
34
|
+
live(articlePath),
|
|
35
|
+
live(commentsPath),
|
|
36
|
+
])
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
In templates access `.value`:
|
|
40
|
+
|
|
41
|
+
```vue
|
|
42
|
+
<h1>{{ article.value?.title }}</h1>
|
|
43
|
+
<div v-for="comment in comments.value" :key="comment.id">
|
|
44
|
+
{{ comment.text }}
|
|
45
|
+
</div>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Step 2 – Load related objects with `.with()`
|
|
49
|
+
|
|
50
|
+
Chain `.with()` to attach related data to each item:
|
|
51
|
+
|
|
52
|
+
```javascript
|
|
53
|
+
const articlesPath = computed(() =>
|
|
54
|
+
path.blog.articlesByCreatedAt({ limit: 20 })
|
|
55
|
+
.with(article => path.userIdentification.identification({
|
|
56
|
+
sessionOrUserType: article.authorType,
|
|
57
|
+
sessionOrUser: article.author
|
|
58
|
+
}).bind('authorProfile'))
|
|
59
|
+
.with(article => path.blog.articleCategory({ category: article.category }).bind('categoryData'))
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
const [articles] = await Promise.all([live(articlesPath)])
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Access in template:
|
|
66
|
+
|
|
67
|
+
```vue
|
|
68
|
+
<div v-for="article in articles.value" :key="article.id">
|
|
69
|
+
<h3>{{ article.title }}</h3>
|
|
70
|
+
<span>by {{ article.authorProfile?.firstName }}</span>
|
|
71
|
+
<Tag :value="article.categoryData?.name" />
|
|
72
|
+
</div>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Nested `.with()` (with inside with)
|
|
76
|
+
|
|
77
|
+
```javascript
|
|
78
|
+
const eventPath = computed(() =>
|
|
79
|
+
path.myService.event({ event: eventId })
|
|
80
|
+
.with(event => path.myService.eventState({ event: event.id }).bind('state')
|
|
81
|
+
.with(state => path.myService.roundPairs({
|
|
82
|
+
event: event.id,
|
|
83
|
+
round: state.round
|
|
84
|
+
}).bind('roundPairs'))
|
|
85
|
+
)
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Step 3 – Conditional loading with `useClient`
|
|
90
|
+
|
|
91
|
+
Use `useClient()` to check authentication state and conditionally build paths:
|
|
92
|
+
|
|
93
|
+
```javascript
|
|
94
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
95
|
+
|
|
96
|
+
const client = useClient()
|
|
97
|
+
|
|
98
|
+
// Path that only loads for logged-in users (null path = no fetch)
|
|
99
|
+
const myDataPath = computed(() =>
|
|
100
|
+
client.value.user && path.blog.myArticles({})
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
// Path that only loads for admins
|
|
104
|
+
const adminPath = computed(() =>
|
|
105
|
+
client.value.roles.includes('admin') && path.blog.allArticles({})
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
const [myData, adminData] = await Promise.all([
|
|
109
|
+
live(myDataPath),
|
|
110
|
+
live(adminPath),
|
|
111
|
+
])
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
When the path is `null` / `false` / `undefined`, `live()` returns a ref with `null` value and does not subscribe.
|
|
115
|
+
|
|
116
|
+
### Conditional rendering in templates
|
|
117
|
+
|
|
118
|
+
```vue
|
|
119
|
+
<template>
|
|
120
|
+
<!-- Admin-only button -->
|
|
121
|
+
<Button v-if="client.roles.includes('admin')"
|
|
122
|
+
label="Create" icon="pi pi-plus" @click="create" />
|
|
123
|
+
|
|
124
|
+
<!-- Show different content based on auth -->
|
|
125
|
+
<div v-if="client.user">
|
|
126
|
+
<!-- Authenticated content -->
|
|
127
|
+
</div>
|
|
128
|
+
<div v-else>
|
|
129
|
+
<p>Please sign in to see your articles.</p>
|
|
130
|
+
<router-link :to="{ name: 'user:signIn' }">Sign in</router-link>
|
|
131
|
+
</div>
|
|
132
|
+
</template>
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Step 4 – Dependent paths
|
|
136
|
+
|
|
137
|
+
When one path depends on data from another, load them sequentially:
|
|
138
|
+
|
|
139
|
+
```javascript
|
|
140
|
+
// First load
|
|
141
|
+
const [article] = await Promise.all([live(articlePath)])
|
|
142
|
+
|
|
143
|
+
// Dependent path using data from first load
|
|
144
|
+
const authorPath = computed(() =>
|
|
145
|
+
article.value && path.userIdentification.identification({
|
|
146
|
+
sessionOrUserType: article.value.authorType,
|
|
147
|
+
sessionOrUser: article.value.author
|
|
148
|
+
})
|
|
149
|
+
)
|
|
150
|
+
const [author] = await Promise.all([live(authorPath)])
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Or use `.with()` to combine them in a single query (preferred when possible).
|
|
154
|
+
|
|
155
|
+
## Step 5 – Props-based paths in components
|
|
156
|
+
|
|
157
|
+
When building reusable components that receive IDs as props:
|
|
158
|
+
|
|
159
|
+
```vue
|
|
160
|
+
<script setup>
|
|
161
|
+
import { computed } from 'vue'
|
|
162
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
163
|
+
|
|
164
|
+
const props = defineProps({
|
|
165
|
+
articleId: { type: String, required: true }
|
|
166
|
+
})
|
|
167
|
+
|
|
168
|
+
const path = usePath()
|
|
169
|
+
|
|
170
|
+
const articlePath = computed(() => path.blog.article({ article: props.articleId }))
|
|
171
|
+
const [article] = await Promise.all([live(articlePath)])
|
|
172
|
+
</script>
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
## Summary
|
|
176
|
+
|
|
177
|
+
| Pattern | When to use |
|
|
178
|
+
|---|---|
|
|
179
|
+
| `computed(() => path.xxx(...))` | Path depends on reactive values |
|
|
180
|
+
| `.with(item => path.yyy(...).bind('field'))` | Attach related objects |
|
|
181
|
+
| `client.value.user && path.xxx(...)` | Load only when authenticated |
|
|
182
|
+
| `client.value.roles.includes('admin')` | Load/show only for specific roles |
|
|
183
|
+
| Sequential `live()` calls | Path depends on data from previous load |
|