@live-change/frontend-template 0.9.197 → 0.9.199
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 +184 -0
- package/.claude/rules/live-change-backend-architecture.md +126 -0
- package/.claude/rules/live-change-backend-models-and-relations.md +188 -0
- package/.claude/rules/live-change-frontend-vue-primevue.md +291 -0
- package/.claude/rules/live-change-service-structure.md +89 -0
- package/.claude/skills/create-skills-and-rules.md +196 -0
- package/.claude/skills/live-change-design-actions-views-triggers.md +190 -0
- package/.claude/skills/live-change-design-models-relations.md +173 -0
- package/.claude/skills/live-change-design-service.md +132 -0
- package/.claude/skills/live-change-frontend-action-buttons.md +128 -0
- package/.claude/skills/live-change-frontend-action-form.md +143 -0
- package/.claude/skills/live-change-frontend-analytics.md +146 -0
- package/.claude/skills/live-change-frontend-command-forms.md +215 -0
- package/.claude/skills/live-change-frontend-data-views.md +182 -0
- package/.claude/skills/live-change-frontend-editor-form.md +177 -0
- package/.claude/skills/live-change-frontend-locale-time.md +171 -0
- package/.claude/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.claude/skills/live-change-frontend-range-list.md +128 -0
- package/.claude/skills/live-change-frontend-ssr-setup.md +118 -0
- package/.cursor/rules/live-change-backend-actions-views-triggers.mdc +202 -0
- package/.cursor/rules/live-change-backend-architecture.mdc +131 -0
- package/.cursor/rules/live-change-backend-models-and-relations.mdc +194 -0
- package/.cursor/rules/live-change-frontend-vue-primevue.mdc +290 -0
- package/.cursor/rules/live-change-service-structure.mdc +107 -0
- package/.cursor/skills/live-change-design-actions-views-triggers.md +197 -0
- package/.cursor/skills/live-change-design-models-relations.md +168 -0
- package/.cursor/skills/live-change-design-service.md +75 -0
- package/.cursor/skills/live-change-frontend-action-buttons.md +128 -0
- package/.cursor/skills/live-change-frontend-action-form.md +143 -0
- package/.cursor/skills/live-change-frontend-analytics.md +146 -0
- package/.cursor/skills/live-change-frontend-command-forms.md +215 -0
- package/.cursor/skills/live-change-frontend-data-views.md +182 -0
- package/.cursor/skills/live-change-frontend-editor-form.md +177 -0
- package/.cursor/skills/live-change-frontend-locale-time.md +171 -0
- package/.cursor/skills/live-change-frontend-page-list-detail.md +200 -0
- package/.cursor/skills/live-change-frontend-range-list.md +128 -0
- package/.cursor/skills/live-change-frontend-ssr-setup.md +119 -0
- package/README.md +71 -0
- package/package.json +50 -50
- package/server/app.config.js +35 -0
- package/server/services.list.js +2 -0
- package/.nx/workspace-data/file-map.json +0 -195
- package/.nx/workspace-data/nx_files.nxt +0 -0
- package/.nx/workspace-data/project-graph.json +0 -8
- package/.nx/workspace-data/project-graph.lock +0 -0
- package/.nx/workspace-data/source-maps.json +0 -1
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Handle locale, timezone, currentTime, time synchronization and email locale
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-locale-time (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you work with **locale, language, time, and timezone** in a LiveChange frontend.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You need to display dates/times in the user's timezone.
|
|
12
|
+
- You are building a time-sensitive feature (countdown, deadline, scheduling).
|
|
13
|
+
- You need to sync locale with vue-i18n.
|
|
14
|
+
- You are writing an email template that must use the recipient's language.
|
|
15
|
+
|
|
16
|
+
## Step 1 – Set up locale in App.vue
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import { watch } from 'vue'
|
|
20
|
+
import { useI18n } from 'vue-i18n'
|
|
21
|
+
import { useLocale } from '@live-change/vue3-components'
|
|
22
|
+
|
|
23
|
+
const { locale: i18nLocale } = useI18n()
|
|
24
|
+
const locale = useLocale()
|
|
25
|
+
|
|
26
|
+
// Capture browser locale settings and save to backend
|
|
27
|
+
locale.captureLocale()
|
|
28
|
+
|
|
29
|
+
// Watch for locale changes and sync with vue-i18n
|
|
30
|
+
locale.getLocaleObservable()
|
|
31
|
+
watch(() => locale.localeRef.value, (newLocale) => {
|
|
32
|
+
if (newLocale?.language && i18nLocale.value !== newLocale.language) {
|
|
33
|
+
i18nLocale.value = newLocale.language
|
|
34
|
+
}
|
|
35
|
+
}, { immediate: true })
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Step 2 – Display dates in user timezone
|
|
39
|
+
|
|
40
|
+
Use vue-i18n's `d()` function for formatting and `locale.localTime()` for SSR timezone conversion:
|
|
41
|
+
|
|
42
|
+
```vue
|
|
43
|
+
<template>
|
|
44
|
+
<span>{{ d(locale.localTime(new Date(article.createdAt)), 'long') }}</span>
|
|
45
|
+
</template>
|
|
46
|
+
|
|
47
|
+
<script setup>
|
|
48
|
+
import { useI18n } from 'vue-i18n'
|
|
49
|
+
import { useLocale } from '@live-change/vue3-components'
|
|
50
|
+
|
|
51
|
+
const { d } = useI18n()
|
|
52
|
+
const locale = useLocale()
|
|
53
|
+
</script>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`locale.localTime(date)` converts server timestamps to the user's timezone during SSR. On the client (browser), it returns the date unchanged since the browser handles timezone natively.
|
|
57
|
+
|
|
58
|
+
## Step 3 – Use `currentTime` for reactive clocks
|
|
59
|
+
|
|
60
|
+
`currentTime` is a global `Ref<number>` that ticks every 500ms:
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<template>
|
|
64
|
+
<span>{{ formattedTime }}</span>
|
|
65
|
+
</template>
|
|
66
|
+
|
|
67
|
+
<script setup>
|
|
68
|
+
import { computed } from 'vue'
|
|
69
|
+
import { currentTime } from '@live-change/frontend-base'
|
|
70
|
+
import { useI18n } from 'vue-i18n'
|
|
71
|
+
|
|
72
|
+
const { d } = useI18n()
|
|
73
|
+
const formattedTime = computed(() =>
|
|
74
|
+
isNaN(currentTime.value) ? '' : d(new Date(currentTime.value), 'time')
|
|
75
|
+
)
|
|
76
|
+
</script>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Any component reading `currentTime` re-renders automatically every 500ms.
|
|
80
|
+
|
|
81
|
+
## Step 4 – Correct clock skew with `useTimeSynchronization`
|
|
82
|
+
|
|
83
|
+
When the client clock differs from the server clock (important for real-time features like quizzes, countdowns, auctions):
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
import { useTimeSynchronization } from '@live-change/vue3-ssr'
|
|
87
|
+
import { currentTime } from '@live-change/frontend-base'
|
|
88
|
+
|
|
89
|
+
const timeSync = useTimeSynchronization()
|
|
90
|
+
|
|
91
|
+
// Convert client time to server time (reactive computed)
|
|
92
|
+
const serverTime = timeSync.localToServerComputed(currentTime)
|
|
93
|
+
|
|
94
|
+
// Countdown to a server-side deadline
|
|
95
|
+
const timeRemaining = computed(() => deadline.value - serverTime.value)
|
|
96
|
+
const seconds = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000) % 60))
|
|
97
|
+
const minutes = computed(() => Math.max(0, Math.floor(timeRemaining.value / 1000 / 60)))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Enable time synchronization in `config.js`:
|
|
101
|
+
|
|
102
|
+
```javascript
|
|
103
|
+
export default {
|
|
104
|
+
timeSynchronization: true,
|
|
105
|
+
// ...
|
|
106
|
+
}
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**When to use:** Only when clock skew matters – real-time events, countdown timers, time-limited actions. For simple date display, `currentTime` alone is sufficient.
|
|
110
|
+
|
|
111
|
+
### Returned object from `useTimeSynchronization()`:
|
|
112
|
+
|
|
113
|
+
| Property | Description |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `diff` | Server-client time offset in ms |
|
|
116
|
+
| `synchronized` | `true` once sync is complete |
|
|
117
|
+
| `serverToLocal(ts)` | Convert server timestamp to local |
|
|
118
|
+
| `localToServer(ts)` | Convert local timestamp to server |
|
|
119
|
+
| `serverToLocalComputed(ts)` | Reactive computed version |
|
|
120
|
+
| `localToServerComputed(ts)` | Reactive computed version |
|
|
121
|
+
|
|
122
|
+
## Step 5 – Smooth countdown with requestAnimationFrame
|
|
123
|
+
|
|
124
|
+
For sub-500ms precision (e.g. animated countdown knobs):
|
|
125
|
+
|
|
126
|
+
```javascript
|
|
127
|
+
import { ref } from 'vue'
|
|
128
|
+
import { useRafFn } from '@vueuse/core'
|
|
129
|
+
import { useTimeSynchronization } from '@live-change/vue3-ssr'
|
|
130
|
+
|
|
131
|
+
const rafNow = ref(Date.now())
|
|
132
|
+
useRafFn(() => { rafNow.value = Date.now() })
|
|
133
|
+
|
|
134
|
+
const timeSync = useTimeSynchronization()
|
|
135
|
+
const rafServerTime = timeSync.localToServerComputed(rafNow)
|
|
136
|
+
|
|
137
|
+
const countdown = computed(() =>
|
|
138
|
+
Math.max(0, deadline.value - rafServerTime.value)
|
|
139
|
+
)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Step 6 – Locale in email templates
|
|
143
|
+
|
|
144
|
+
Email templates render server-side. They fetch the recipient's locale explicitly:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
import { useI18n } from 'vue-i18n'
|
|
148
|
+
import { useLocale } from '@live-change/vue3-components'
|
|
149
|
+
|
|
150
|
+
const { locale: i18nLocale, t } = useI18n()
|
|
151
|
+
const locale = useLocale()
|
|
152
|
+
|
|
153
|
+
// props.json contains { user, client: { session } }
|
|
154
|
+
const data = JSON.parse(json)
|
|
155
|
+
|
|
156
|
+
// Fetch locale for the recipient (not current session)
|
|
157
|
+
await Promise.all([
|
|
158
|
+
locale.getOtherUserOrSessionLocale(data.user, data.client?.session)
|
|
159
|
+
])
|
|
160
|
+
|
|
161
|
+
// Apply to vue-i18n
|
|
162
|
+
if (locale.getLanguage()) i18nLocale.value = locale.getLanguage()
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Key differences from regular pages:
|
|
166
|
+
|
|
167
|
+
| Aspect | Regular page | Email template |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| Locale source | `locale.getLocale()` (current user) | `locale.getOtherUserOrSessionLocale(user, session)` |
|
|
170
|
+
| Time conversion | `locale.localTime()` only on SSR | Always server-side |
|
|
171
|
+
| Reactive updates | Yes | No (one-shot SSR render) |
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build list and detail pages with live data, computed paths, .with() and useClient
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-page-list-detail (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you need to build a **list + detail** UI in Vue 3 / PrimeVue / Tailwind for a LiveChange backend.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You are adding a new list page (devices, orders, etc.).
|
|
12
|
+
- You are adding a detail page for a single object.
|
|
13
|
+
- You want to follow the `live(path)` + `Promise.all` pattern compatible with SSR.
|
|
14
|
+
|
|
15
|
+
## Step 1 – List page (`src/pages/<resource>/index.vue`)
|
|
16
|
+
|
|
17
|
+
1. Create the file: `src/pages/<resource>/index.vue`.
|
|
18
|
+
2. In the `<template>`, follow this layout:
|
|
19
|
+
- container with padding (`container mx-auto p-4`),
|
|
20
|
+
- header with title and “add” button,
|
|
21
|
+
- empty-state card,
|
|
22
|
+
- grid of cards or rows.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
|
|
26
|
+
```vue
|
|
27
|
+
<template>
|
|
28
|
+
<div class="container mx-auto p-4">
|
|
29
|
+
<div class="flex items-center justify-between mb-6">
|
|
30
|
+
<h1 class="text-2xl font-bold">Devices</h1>
|
|
31
|
+
<Button label="Add" icon="pi pi-plus" @click="openDialog" />
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<Card v-if="devices.value?.length === 0">
|
|
35
|
+
<template #content>
|
|
36
|
+
<p class="text-center text-gray-500">
|
|
37
|
+
No devices yet
|
|
38
|
+
</p>
|
|
39
|
+
</template>
|
|
40
|
+
</Card>
|
|
41
|
+
|
|
42
|
+
<div class="grid gap-4">
|
|
43
|
+
<Card v-for="device in devices.value" :key="device.id">
|
|
44
|
+
<template #content>
|
|
45
|
+
<!-- device content -->
|
|
46
|
+
</template>
|
|
47
|
+
</Card>
|
|
48
|
+
</div>
|
|
49
|
+
</div>
|
|
50
|
+
</template>
|
|
51
|
+
|
|
52
|
+
<script setup>
|
|
53
|
+
import { path, live, api as useApi } from '@live-change/vue3-ssr'
|
|
54
|
+
import Button from 'primevue/button'
|
|
55
|
+
import Card from 'primevue/card'
|
|
56
|
+
|
|
57
|
+
const api = useApi()
|
|
58
|
+
|
|
59
|
+
const [devices] = await Promise.all([
|
|
60
|
+
live(path().deviceManager.myUserDevices({}))
|
|
61
|
+
])
|
|
62
|
+
|
|
63
|
+
function openDialog() {
|
|
64
|
+
// open dialog for creating a new device
|
|
65
|
+
}
|
|
66
|
+
</script>
|
|
67
|
+
|
|
68
|
+
<route>
|
|
69
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
70
|
+
</route>
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
## Step 2 – Detail page (`src/pages/<resource>/[id].vue`)
|
|
74
|
+
|
|
75
|
+
1. Create `src/pages/<resource>/[id].vue`.
|
|
76
|
+
2. In `script setup`:
|
|
77
|
+
- use `useRoute()` to get the id (`route.params.id`),
|
|
78
|
+
- fetch the main object and related data using `Promise.all`.
|
|
79
|
+
|
|
80
|
+
Example skeleton:
|
|
81
|
+
|
|
82
|
+
```vue
|
|
83
|
+
<template>
|
|
84
|
+
<div class="container mx-auto p-4 space-y-4" v-if="item.value">
|
|
85
|
+
<Card>
|
|
86
|
+
<template #title>
|
|
87
|
+
{{ item.value.name }}
|
|
88
|
+
</template>
|
|
89
|
+
<template #content>
|
|
90
|
+
<!-- details -->
|
|
91
|
+
</template>
|
|
92
|
+
</Card>
|
|
93
|
+
</div>
|
|
94
|
+
</template>
|
|
95
|
+
|
|
96
|
+
<script setup>
|
|
97
|
+
import { path, live } from '@live-change/vue3-ssr'
|
|
98
|
+
import { useRoute } from 'vue-router'
|
|
99
|
+
import Card from 'primevue/card'
|
|
100
|
+
|
|
101
|
+
const route = useRoute()
|
|
102
|
+
const id = route.params.id
|
|
103
|
+
|
|
104
|
+
const [item] = await Promise.all([
|
|
105
|
+
live(path().myService.myUserItem({ item: id }))
|
|
106
|
+
])
|
|
107
|
+
</script>
|
|
108
|
+
|
|
109
|
+
<route>
|
|
110
|
+
{ "name": "myItem", "meta": { "signedIn": true } }
|
|
111
|
+
</route>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## Step 3 – Computed paths with reactive parameters
|
|
115
|
+
|
|
116
|
+
When the path depends on reactive values (route params, props), wrap it in `computed()`:
|
|
117
|
+
|
|
118
|
+
```js
|
|
119
|
+
import { computed, unref } from 'vue'
|
|
120
|
+
import { usePath, live } from '@live-change/vue3-ssr'
|
|
121
|
+
import { useRoute } from 'vue-router'
|
|
122
|
+
|
|
123
|
+
const path = usePath()
|
|
124
|
+
const route = useRoute()
|
|
125
|
+
const deviceId = route.params.device
|
|
126
|
+
|
|
127
|
+
const devicePath = computed(() => path.deviceManager.myUserDevice({ device: unref(deviceId) }))
|
|
128
|
+
const connectionsPath = computed(() =>
|
|
129
|
+
path.deviceManager.deviceOwnedDeviceConnections({ device: unref(deviceId) })
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
const [device, connections] = await Promise.all([
|
|
133
|
+
live(devicePath),
|
|
134
|
+
live(connectionsPath),
|
|
135
|
+
])
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Step 4 – Attach related objects with `.with()`
|
|
139
|
+
|
|
140
|
+
Use `.with()` to load related data alongside each item:
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
const devicesPath = computed(() =>
|
|
144
|
+
path.deviceManager.myUserDevices({})
|
|
145
|
+
.with(device => path.deviceManager.deviceState({ device: device.id }).bind('state'))
|
|
146
|
+
.with(device => path.userIdentification.identification({
|
|
147
|
+
sessionOrUserType: device.ownerType,
|
|
148
|
+
sessionOrUser: device.owner
|
|
149
|
+
}).bind('ownerProfile'))
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
const [devices] = await Promise.all([live(devicesPath)])
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Access in template: `device.state?.online`, `device.ownerProfile?.firstName`.
|
|
156
|
+
|
|
157
|
+
## Step 5 – Auth guard with `useClient`
|
|
158
|
+
|
|
159
|
+
Use `useClient()` to conditionally show UI or load data based on authentication:
|
|
160
|
+
|
|
161
|
+
```js
|
|
162
|
+
import { useClient } from '@live-change/vue3-ssr'
|
|
163
|
+
|
|
164
|
+
const client = useClient()
|
|
165
|
+
|
|
166
|
+
// Conditional path (null = no fetch)
|
|
167
|
+
const adminDataPath = computed(() =>
|
|
168
|
+
client.value.roles.includes('admin') && path.deviceManager.allDevices({})
|
|
169
|
+
)
|
|
170
|
+
const [adminData] = await Promise.all([live(adminDataPath)])
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Template:
|
|
174
|
+
|
|
175
|
+
```vue
|
|
176
|
+
<Button v-if="client.roles.includes('admin')" label="Admin panel" />
|
|
177
|
+
<div v-if="!client.user">
|
|
178
|
+
<p>Please sign in</p>
|
|
179
|
+
<router-link :to="{ name: 'user:signIn' }">Sign in</router-link>
|
|
180
|
+
</div>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Step 6 – Status tags and simple indicators
|
|
184
|
+
|
|
185
|
+
1. Use PrimeVue `Tag` for status fields.
|
|
186
|
+
2. Map statuses to severities (`success`, `secondary`, etc.).
|
|
187
|
+
|
|
188
|
+
Example:
|
|
189
|
+
|
|
190
|
+
```vue
|
|
191
|
+
<Tag
|
|
192
|
+
:value="conn.status"
|
|
193
|
+
:severity="conn.status === 'online' ? 'success' : 'secondary'"
|
|
194
|
+
/>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
## Step 7 – Large lists with RangeViewer
|
|
198
|
+
|
|
199
|
+
For large or infinite-scroll lists, use `<RangeViewer>` instead of loading everything at once. See the `live-change-frontend-range-list` skill for details.
|
|
200
|
+
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Build paginated scrollable lists with RangeViewer, rangeBuckets and .with()
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-range-list (Claude Code)
|
|
6
|
+
|
|
7
|
+
Use this skill when you build **paginated, scrollable lists** backed by DAO ranges in a LiveChange frontend.
|
|
8
|
+
|
|
9
|
+
## When to use
|
|
10
|
+
|
|
11
|
+
- You need a list that loads items in pages (infinite scroll).
|
|
12
|
+
- The list is backed by a DAO range view (e.g. `articlesByCreatedAt`).
|
|
13
|
+
- You want to attach related objects to each item via `.with()`.
|
|
14
|
+
|
|
15
|
+
## Step 1 – Define the path function
|
|
16
|
+
|
|
17
|
+
The path function receives a `range` object (with `gt`, `gte`, `lt`, `lte`, `limit`, `reverse`) and returns a DAO path.
|
|
18
|
+
|
|
19
|
+
Use `reverseRange()` to display items newest-first:
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
import { reverseRange } from '@live-change/vue3-ssr'
|
|
23
|
+
|
|
24
|
+
function articlesPathRange(range) {
|
|
25
|
+
return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
|
|
26
|
+
}
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Step 2 – Attach related objects with `.with()`
|
|
30
|
+
|
|
31
|
+
Chain `.with()` calls to load related data for each item:
|
|
32
|
+
|
|
33
|
+
```javascript
|
|
34
|
+
function articlesPathRange(range) {
|
|
35
|
+
return path.blog.articlesByCreatedAt({ ...reverseRange(range) })
|
|
36
|
+
.with(article => path.userIdentification.identification({
|
|
37
|
+
sessionOrUserType: article.authorType,
|
|
38
|
+
sessionOrUser: article.author
|
|
39
|
+
}).bind('authorProfile'))
|
|
40
|
+
.with(article => path.blog.articleStats({ article: article.id }).bind('stats'))
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Each `.with()` call:
|
|
45
|
+
- receives a proxy of the item,
|
|
46
|
+
- builds a path to the related data,
|
|
47
|
+
- calls `.bind('fieldName')` to attach the result under that field name.
|
|
48
|
+
|
|
49
|
+
Nested `.with()` is also supported:
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
function eventsPathRange(range) {
|
|
53
|
+
return path.myService.allEvents({ ...reverseRange(range) })
|
|
54
|
+
.with(event => path.myService.eventState({ event: event.id }).bind('state')
|
|
55
|
+
.with(state => path.myService.roundPairs({ event: event.id, round: state.round }).bind('roundPairs'))
|
|
56
|
+
)
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Step 3 – Use `<RangeViewer>` in the template
|
|
61
|
+
|
|
62
|
+
```vue
|
|
63
|
+
<template>
|
|
64
|
+
<RangeViewer
|
|
65
|
+
:pathFunction="articlesPathRange"
|
|
66
|
+
:canLoadTop="false"
|
|
67
|
+
canDropBottom
|
|
68
|
+
loadBottomSensorSize="3000px"
|
|
69
|
+
dropBottomSensorSize="5000px"
|
|
70
|
+
>
|
|
71
|
+
<template #empty>
|
|
72
|
+
<p class="text-center text-surface-500 my-4">No articles yet.</p>
|
|
73
|
+
</template>
|
|
74
|
+
<template #default="{ item: article }">
|
|
75
|
+
<Card class="mb-2">
|
|
76
|
+
<template #content>
|
|
77
|
+
<h3>{{ article.title }}</h3>
|
|
78
|
+
<p class="text-sm text-surface-500">By {{ article.authorProfile?.firstName }}</p>
|
|
79
|
+
</template>
|
|
80
|
+
</Card>
|
|
81
|
+
</template>
|
|
82
|
+
</RangeViewer>
|
|
83
|
+
</template>
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Key props:
|
|
87
|
+
|
|
88
|
+
| Prop | Default | Description |
|
|
89
|
+
|---|---|---|
|
|
90
|
+
| `pathFunction` | required | `(range) => path` – builds the DAO path for a given range |
|
|
91
|
+
| `bucketSize` | `20` | Items per page |
|
|
92
|
+
| `canLoadTop` / `canLoadBottom` | `true` | Whether loading in each direction is allowed |
|
|
93
|
+
| `canDropTop` / `canDropBottom` | `false` | Whether to drop pages that scrolled far out of view |
|
|
94
|
+
| `loadBottomSensorSize` | `'500px'` | How far before the bottom to trigger loading (increase for smoother UX) |
|
|
95
|
+
| `dropBottomSensorSize` | `'5000px'` | How far to keep before dropping |
|
|
96
|
+
| `frozen` | `false` | Pause live updates |
|
|
97
|
+
|
|
98
|
+
Slots:
|
|
99
|
+
|
|
100
|
+
| Slot | Props | Description |
|
|
101
|
+
|---|---|---|
|
|
102
|
+
| `default` | `{ item, bucket, itemIndex, bucketIndex }` | Render each item |
|
|
103
|
+
| `empty` | – | Shown when there are no items |
|
|
104
|
+
| `loadingTop` / `loadingBottom` | – | Loading spinners |
|
|
105
|
+
| `changedTop` / `changedBottom` | – | Indicators when data changed while frozen |
|
|
106
|
+
|
|
107
|
+
## Step 4 – Low-level `rangeBuckets` (optional)
|
|
108
|
+
|
|
109
|
+
For advanced control, use `rangeBuckets` directly:
|
|
110
|
+
|
|
111
|
+
```javascript
|
|
112
|
+
import { rangeBuckets, reverseRange } from '@live-change/vue3-ssr'
|
|
113
|
+
|
|
114
|
+
const { buckets, loadBottom, dropTop, freeze, unfreeze } = await rangeBuckets(
|
|
115
|
+
(range) => path.blog.articlesByCreatedAt({ ...reverseRange(range) }),
|
|
116
|
+
{ bucketSize: 20 }
|
|
117
|
+
)
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Iterate in the template:
|
|
121
|
+
|
|
122
|
+
```vue
|
|
123
|
+
<template v-for="(bucket, bi) in buckets.value" :key="bi">
|
|
124
|
+
<div v-for="(item, ii) in bucket.data.value" :key="item.id">
|
|
125
|
+
<!-- render item -->
|
|
126
|
+
</div>
|
|
127
|
+
</template>
|
|
128
|
+
```
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Set up SSR entry points, router, PrimeVue theme and Suspense data loading
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# Skill: live-change-frontend-ssr-setup
|
|
6
|
+
|
|
7
|
+
Ten skill opisuje **konfigurację frontendu** opartego o live-change-stack:
|
|
8
|
+
|
|
9
|
+
- entry-client / entry-server,
|
|
10
|
+
- router i meta `signedIn`,
|
|
11
|
+
- konfigurację PrimeVue / motywu.
|
|
12
|
+
|
|
13
|
+
## 1. Entry points – klient i serwer
|
|
14
|
+
|
|
15
|
+
1. W projekcie frontendowym upewnij się, że masz dwa entry points:
|
|
16
|
+
- `entry-client.js` (lub `.ts`),
|
|
17
|
+
- `entry-server.js` (lub `.ts`).
|
|
18
|
+
|
|
19
|
+
2. Skorzystaj z helperów z `@live-change/frontend-base`:
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
// entry-client.js
|
|
23
|
+
import { clientEntry } from '@live-change/frontend-base/client-entry.js'
|
|
24
|
+
import App from './App.vue'
|
|
25
|
+
import { createRouter } from './router.js'
|
|
26
|
+
import { config } from './config.js'
|
|
27
|
+
|
|
28
|
+
export default clientEntry(App, createRouter, config)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
// entry-server.js
|
|
33
|
+
import { serverEntry, sitemapEntry } from '@live-change/frontend-base/server-entry.js'
|
|
34
|
+
import App from './App.vue'
|
|
35
|
+
import { createRouter, routerSitemap } from './router.js'
|
|
36
|
+
import { config } from './config.js'
|
|
37
|
+
|
|
38
|
+
export const render = serverEntry(App, createRouter, config)
|
|
39
|
+
export const sitemap = sitemapEntry(App, createRouter, routerSitemap, config)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 2. Router – `vite-plugin-pages` + meta `signedIn`
|
|
43
|
+
|
|
44
|
+
1. Użyj `vite-plugin-pages` do generowania tras z `src/pages/`.
|
|
45
|
+
2. Dodaj blok `<route>` w plikach stron, np.:
|
|
46
|
+
|
|
47
|
+
```vue
|
|
48
|
+
<route>
|
|
49
|
+
{ "name": "devices", "meta": { "signedIn": true } }
|
|
50
|
+
</route>
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
3. W routerze dodaj guard logowania (jeśli go nie ma):
|
|
54
|
+
|
|
55
|
+
```js
|
|
56
|
+
router.beforeEach((to) => {
|
|
57
|
+
if(to.meta.signedIn && !isLoggedIn()) {
|
|
58
|
+
localStorage.setItem('redirectAfterLogin', to.fullPath)
|
|
59
|
+
return { name: 'user:signIn' }
|
|
60
|
+
}
|
|
61
|
+
})
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Funkcja `isLoggedIn()` może opierać się na stanie sesji/usera w store lub prostym sprawdzeniu tokena/ciasteczek.
|
|
65
|
+
|
|
66
|
+
## 3. Konfiguracja PrimeVue – motyw i opcje
|
|
67
|
+
|
|
68
|
+
1. W pliku `config.js` skonfiguruj PrimeVue:
|
|
69
|
+
|
|
70
|
+
```js
|
|
71
|
+
import { definePreset } from '@primevue/themes'
|
|
72
|
+
import Aura from '@primevue/themes/aura'
|
|
73
|
+
|
|
74
|
+
const MyPreset = definePreset(Aura, {
|
|
75
|
+
semantic: {
|
|
76
|
+
primary: {
|
|
77
|
+
50: '{indigo.50}',
|
|
78
|
+
100: '{indigo.100}',
|
|
79
|
+
200: '{indigo.200}',
|
|
80
|
+
300: '{indigo.300}',
|
|
81
|
+
400: '{indigo.400}',
|
|
82
|
+
500: '{indigo.500}',
|
|
83
|
+
600: '{indigo.600}',
|
|
84
|
+
700: '{indigo.700}',
|
|
85
|
+
800: '{indigo.800}',
|
|
86
|
+
900: '{indigo.900}',
|
|
87
|
+
950: '{indigo.950}'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
export const config = {
|
|
93
|
+
theme: {
|
|
94
|
+
preset: MyPreset,
|
|
95
|
+
options: {
|
|
96
|
+
darkModeSelector: '.app-dark-mode'
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
2. Upewnij się, że App.vue używa tego configu przez globalną konfigurację PrimeVue (zgodnie z template’em live-change-stack).
|
|
103
|
+
|
|
104
|
+
## 4. Globalne komponenty i formularze
|
|
105
|
+
|
|
106
|
+
1. W `App.vue` zarejestruj globalne komponenty używane często w projekcie:
|
|
107
|
+
- auto-form components,
|
|
108
|
+
- globalne layouty, jeśli są.
|
|
109
|
+
|
|
110
|
+
2. Dzięki temu na stronach używasz np. `<command-form>` bez lokalnego importu.
|
|
111
|
+
|
|
112
|
+
## 5. SSR i `live/path` + Suspense
|
|
113
|
+
|
|
114
|
+
1. Upewnij się, że root aplikacji (np. `ViewRoot`) opakowuje strony w `<Suspense>`.
|
|
115
|
+
2. Strony powinny:
|
|
116
|
+
- używać `await Promise.all([live(path()....)])` w `script setup`,
|
|
117
|
+
- korzystać z `.value` w template,
|
|
118
|
+
- nie wykonywać pobierania danych w `onMounted`.
|
|
119
|
+
|
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: @live-change/frontend-template
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
## @live-change/frontend-template
|
|
6
|
+
|
|
7
|
+
`@live-change/frontend-template` to **szablon kompletnej aplikacji frontendowej** Live Change:
|
|
8
|
+
|
|
9
|
+
- gotowy SSR + SPA oparty o `@live-change/frontend-base` i `@live-change/vue3-ssr`
|
|
10
|
+
- zintegrowane moduły: `user-frontend`, `security`, `content`, `blog`, `task`, `upload`, `url`, `video-call`, `peer-connection`, `auto-form` i inne
|
|
11
|
+
- standardowa struktura katalogów i skrypty `package.json`
|
|
12
|
+
|
|
13
|
+
Szablon jest punktem startowym dla nowych projektów (np. `family-tree`, `speed-dating` zostały na nim oparte).
|
|
14
|
+
|
|
15
|
+
### Najważniejsze zależności
|
|
16
|
+
|
|
17
|
+
W `package.json` szablonu znajdziesz m.in.:
|
|
18
|
+
|
|
19
|
+
- `@live-change/frontend-base`
|
|
20
|
+
- `@live-change/frontend-auto-form`
|
|
21
|
+
- `@live-change/vue3-components`
|
|
22
|
+
- `@live-change/vue3-ssr`
|
|
23
|
+
- frontendy: `user-frontend`, `access-control-frontend`, `content-frontend`, `image-frontend`, `task-frontend`, `upload-frontend`, `url-frontend`, `wysiwyg-frontend`, `blog-frontend`, `video-call-frontend`, `peer-connection-frontend`
|
|
24
|
+
|
|
25
|
+
### Skrypty uruchomieniowe
|
|
26
|
+
|
|
27
|
+
Typowe skrypty (uproszczony przegląd):
|
|
28
|
+
|
|
29
|
+
- `memDev` / `localDev` / `dev` – tryby deweloperskie z różną konfiguracją DB
|
|
30
|
+
- `ssrDev` – dev SSR
|
|
31
|
+
- `serveAll` / `serveAllMem` – produkcyjny SSR z API i usługami
|
|
32
|
+
- `apiServer`, `devApiServer`, `memApiServer` – warianty samego API
|
|
33
|
+
- `build`, `build:client`, `build:ssr`, `build:server`, `build:spa` – budowanie frontu i serwera
|
|
34
|
+
- `prerender*` – generowanie statycznych stron
|
|
35
|
+
|
|
36
|
+
Są one spójne z opisem w dokumentacji serwera (`server/01-getting-started.md`).
|
|
37
|
+
|
|
38
|
+
### Struktura projektu na bazie szablonu
|
|
39
|
+
|
|
40
|
+
Typowy projekt zaczynający z `frontend-template` ma:
|
|
41
|
+
|
|
42
|
+
- `server/app.config.js` – lista usług i konfiguracja klienta
|
|
43
|
+
- `server/services.list.js` – eksport definicji usług
|
|
44
|
+
- `server/start.js` – start CLI (opisany w dokumentacji serwera)
|
|
45
|
+
- `front/src` – kod frontendu:
|
|
46
|
+
- `App.vue`
|
|
47
|
+
- `router.js`
|
|
48
|
+
- `config.js` (i18n, tematy, integracje)
|
|
49
|
+
- `pages/*` – strony routingu (`vite-plugin-pages`)
|
|
50
|
+
- `components/*` – komponenty wspólne
|
|
51
|
+
|
|
52
|
+
W `front/src/config.js` łączysz lokalizacje `frontend-base`, `frontend-auto-form` i innych modułów, podobnie jak w:
|
|
53
|
+
|
|
54
|
+
- `family-tree/front/src/config.js`
|
|
55
|
+
- `speed-dating/front/src/config.js`
|
|
56
|
+
|
|
57
|
+
### Praca z szablonem
|
|
58
|
+
|
|
59
|
+
Typowy flow tworzenia nowej aplikacji:
|
|
60
|
+
|
|
61
|
+
1. Tworzysz nowy pakiet oparty o `@live-change/frontend-template` (lub kopiujesz repo).
|
|
62
|
+
2. Aktualizujesz `server/app.config.js` i `server/services.list.js`, aby włączyć tylko potrzebne usługi.
|
|
63
|
+
3. Dostosowujesz `front/src/config.js` (temat, i18n, integracje analityki).
|
|
64
|
+
4. Tworzysz nowe strony w `front/src/pages` oraz komponenty w `front/src/components`.
|
|
65
|
+
5. Korzystasz z:
|
|
66
|
+
- `@live-change/vue3-ssr` do pracy z DAO,
|
|
67
|
+
- `@live-change/vue3-components` do logiki i formularzy,
|
|
68
|
+
- `@live-change/frontend-auto-form` do CRUD-ów.
|
|
69
|
+
|
|
70
|
+
Dokładny „manual” tego flow znajdzie się w sekcji `frontend/01-getting-started.md` dokumentacji.
|
|
71
|
+
|