Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
dist
node_modules
coverage
.nuxt
.output
.data
.vscode
.DS_Store
.eslintcache
*.tsbuildinfo
*.log*
*.env*

!playground/.env
!test/fixtures/_shared/.env
!test/fixtures/vue/.env
!test/fixtures/nuxt/.env
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ const { data: users } = useQuery(() => zero.value.query.user)
```

> [!TIP]
> See [the playground](./playground) for a full working example based on [rocicorp/hello-zero](https://github.com/rocicorp/hello-zero), or check out [danielroe/hello-zero-nuxt](https://github.com/danielroe/hello-zero-nuxt) to see how to set things up with [Nuxt](https://nuxt.com/).
> See [the Vue fixture](./test/fixtures/vue) or [the Nuxt fixture](./test/fixtures/nuxt) for full working examples based on [rocicorp/hello-zero](https://github.com/rocicorp/hello-zero).

## 💻 Development

Expand Down
2 changes: 1 addition & 1 deletion knip.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"$schema": "https://unpkg.com/knip@5/schema.json",
"ignoreWorkspaces": [
"playground"
"test/fixtures/*"
]
}
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@
"prepare": "simple-git-hooks",
"prepack": "pnpm build",
"prepublishOnly": "pnpm lint && pnpm test",
"test": "pnpm test:unit && pnpm test:types",
"test": "pnpm test:unit && pnpm test:types && pnpm test:e2e",
"test:e2e": "pnpm -r --workspace-concurrency=1 test:e2e",
"test:unit": "vitest",
"test:types": "tsc --noEmit && pnpm -r test:types"
},
Expand Down
5,766 changes: 5,675 additions & 91 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

5 changes: 4 additions & 1 deletion pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ trustPolicy: no-downgrade

trustPolicyExclude:
- chokidar@4.0.3
- semver@6.3.1

packages:
- playground
- test/fixtures/vue
- test/fixtures/nuxt

overrides:
zero-vue: 'link:.'

allowBuilds:
'@parcel/watcher': false
'@rocicorp/zero-sqlite3': true
esbuild: false
oxc-resolver: true
Expand Down
3 changes: 0 additions & 3 deletions playground/.env → test/fixtures/_shared/.env
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,5 @@ ZERO_QUERY_FORWARD_COOKIES="true"
# ZERO_QUERY_URL="http://localhost:3000/api/zero/query"

# ZERO_AUTH_SECRET="secretkey"
# VITE_PUBLIC_AUTH_SECRET='secretkey'

ZERO_REPLICA_FILE="/tmp/zstart_replicas.db"

# VITE_PUBLIC_ZERO_CACHE_URL='http://localhost:4848'
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Medium, Message, User } from '~/db/schema'
import { randBetween, randID, randInt } from '~/utils/rand'
import type { Medium, Message, User } from '../schema'
import { randBetween, randID, randInt } from '../../utils/rand'

const requests = [
'Hey guys, is the zero package ready yet?',
Expand Down
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
2 changes: 2 additions & 0 deletions test/fixtures/nuxt/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# NUXT_ZERO_AUTH_SECRET="secretkey"
# NUXT_PUBLIC_ZERO_CACHE_URL="http://localhost:4848"
1 change: 1 addition & 0 deletions test/fixtures/nuxt/.nuxtrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
setups.@nuxt/test-utils="4.0.3"
File renamed without changes.
250 changes: 250 additions & 0 deletions test/fixtures/nuxt/app/app.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
<script setup lang="ts">
import { useInterval } from '#fx/composables/use-interval'
import { randomMessage } from '#fx/db/data/test-data'
import { formatDate } from '#fx/utils/date'
import { randInt } from '#fx/utils/rand'
import { mutators, queries } from '~/composables/zero'

const zero = useZero()
const { data: users } = useQuery(() => queries.users.all())
const { data: mediums } = useQuery(() => queries.mediums.all())
const { data: allMessages } = useQuery(() => queries.messages.all())

const filterUser = ref('')
const filterText = ref('')
const action = ref<'add' | 'remove' | undefined>(undefined)

const { data: filteredMessages } = useQuery(() => queries.messages.filtered({
filterUser: filterUser.value,
filterText: filterText.value,
}))

const hasFilters = computed(() => filterUser.value || filterText.value)

function deleteRandomMessage() {
if (allMessages.value.length === 0) {
return false
}
const index = randInt(allMessages.value.length)
zero.value.mutate(mutators.message.delete({ id: allMessages.value[index]!.id }))

return true
}

function addRandomMessage() {
zero.value.mutate(mutators.message.insert(
randomMessage(users.value, mediums.value),
))
return true
}

function handleAction() {
if (action.value === 'add') {
return addRandomMessage()
}
else if (action.value === 'remove') {
return deleteRandomMessage()
}

return false
}

useInterval(
() => {
if (!handleAction()) {
action.value = undefined
}
},
() => action.value !== undefined ? 1000 / 60 : null,
)

const INITIAL_HOLD_DELAY_MS = 300
let holdTimer: ReturnType<typeof setTimeout> | null = null
function handleAddAction() {
addRandomMessage()
holdTimer = setTimeout(() => {
action.value = 'add'
}, INITIAL_HOLD_DELAY_MS)
}

function handleRemoveAction(e: MouseEvent | TouchEvent) {
if (zero.value.userID === 'anon' && 'shiftKey' in e && !e.shiftKey) {
// eslint-disable-next-line no-alert
alert('You must be logged in to delete. Hold shift to try anyway.')
return
}
deleteRandomMessage()

holdTimer = setTimeout(() => {
action.value = 'remove'
}, INITIAL_HOLD_DELAY_MS)
}

function stopAction() {
if (holdTimer) {
clearTimeout(holdTimer)
holdTimer = null
}

action.value = undefined
}

function editMessage(e: MouseEvent, id: string, senderID: string, prev: string) {
if (senderID !== zero.value.userID && !e.shiftKey) {
// eslint-disable-next-line no-alert
alert('You aren\'t logged in as the sender of this message. Editing won\'t be permitted. Hold the shift key to try anyway.')
return
}

// eslint-disable-next-line no-alert
const body = prompt('Edit message', prev)
zero.value.mutate(mutators.message.update({
id,
body: body ?? prev,
}))
}

const jwt = useCookie('jwt')
async function toggleLogin() {
if (zero.value.userID === 'anon') {
await $fetch('/api/login')
}
else {
jwt.value = null
}
location.reload()
}

const user = computed(() => users.value.find(user => user.id === zero.value.userID)?.name ?? 'anon')
</script>

<template>
<div>
<div class="controls">
<div>
<button
@mousedown="handleAddAction"
@mouseup="stopAction"
@mouseleave="stopAction"
@touchstart="handleAddAction"
@touchend="stopAction"
>
Add Messages
</button>
<button
@mousedown="handleRemoveAction"
@mouseup="stopAction"
@mouseleave="stopAction"
@touchstart="handleRemoveAction"
@touchend="stopAction"
>
Remove Messages
</button>
<em>(hold down buttons to repeat)</em>
</div>
<div :style="{ justifyContent: 'end' }">
{{ user === 'anon' ? '' : `Logged in as ${user}` }}
<button @mousedown="toggleLogin">
{{ user === 'anon' ? 'Login' : 'Logout' }}
</button>
</div>
</div>
<div class="controls">
<div>
From:
<select
v-model="filterUser"
:style="{ flex: 1 }"
>
<option
key=""
value=""
>
Sender
</option>
<option
v-for="choice in users"
:key="choice.id"
:value="choice.id"
>
{{ choice.name }}
</option>
</select>
</div>
<div>
Contains:
<input
v-model="filterText"
type="text"
placeholder="message"
:style="{ flex: 1 }"
>
</div>
</div>
<div class="controls">
<em>
<template v-if="!hasFilters">
Showing all {{ filteredMessages.length }} messages
</template>
<template v-else>
Showing {{ filteredMessages.length }} of {{ allMessages.length }} messages. Try opening
<a
href="/"
target="_blank"
>another tab</a> to see them all!
</template>
</em>
</div>
<template v-if="filteredMessages.length === 0">
<h3><em>No posts found 😢</em></h3>
</template>
<template v-else>
<table
border="1"
cellspacing="0"
cellpadding="6"
width="100%"
>
<thead>
<tr>
<th>Sender</th>
<th>Medium</th>
<th>Message</th>
<th>Sent</th>
<th>Edit</th>
</tr>
</thead>
<tbody>
<tr
v-for="message in filteredMessages"
:key="message.id"
>
<td align="left">
{{ message.sender?.name }}
</td>
<td align="left">
{{ message.medium?.name }}
</td>
<td align="left">
{{ message.body }}
</td>
<td align="right">
{{ formatDate(message.timestamp) }}
</td>
<td @mousedown="(e: MouseEvent) => editMessage(e, message.id, message.senderID, message.body)">
✏️
</td>
</tr>
</tbody>
</table>
</template>
</div>
</template>

<style scoped>
.controls {
display: flex;
justify-content: space-between;
margin-bottom: 1em;
}
</style>
File renamed without changes.
Loading