@tthr/vue 0.0.7 → 0.0.9
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/dist/index.d.ts +23 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -2
- package/dist/index.js.map +1 -1
- package/nuxt/runtime/components/TetherWelcome.vue +357 -24
- package/nuxt/runtime/composables.ts +25 -10
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -65,6 +65,22 @@ export interface MutationFunction<TArgs = void, TResult = unknown> {
|
|
|
65
65
|
_args?: TArgs;
|
|
66
66
|
_result?: TResult;
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* Options for useMutation
|
|
70
|
+
*/
|
|
71
|
+
export interface UseMutationOptions {
|
|
72
|
+
/**
|
|
73
|
+
* Query names to invalidate after mutation succeeds.
|
|
74
|
+
* This will trigger a refetch for all active subscriptions matching these queries.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // When creating a comment, also refresh the posts list to update comment counts
|
|
78
|
+
* const createComment = useMutation(api.comments.create, {
|
|
79
|
+
* invalidates: ['posts.list', 'posts.byId']
|
|
80
|
+
* });
|
|
81
|
+
*/
|
|
82
|
+
invalidates?: string[];
|
|
83
|
+
}
|
|
68
84
|
/**
|
|
69
85
|
* Mutation composable
|
|
70
86
|
*
|
|
@@ -76,11 +92,16 @@ export interface MutationFunction<TArgs = void, TResult = unknown> {
|
|
|
76
92
|
*
|
|
77
93
|
* const createPost = useMutation(api.posts.create);
|
|
78
94
|
* await createPost.mutate({ title: 'Hello', content: '...' });
|
|
95
|
+
*
|
|
96
|
+
* // With query invalidation - refreshes posts list when a comment is added
|
|
97
|
+
* const createComment = useMutation(api.comments.create, {
|
|
98
|
+
* invalidates: ['posts.list']
|
|
99
|
+
* });
|
|
79
100
|
* </script>
|
|
80
101
|
* ```
|
|
81
102
|
*/
|
|
82
|
-
export declare function useMutation<TArgs, TResult>(mutation: MutationFunction<TArgs, TResult
|
|
83
|
-
export type { TetherClientOptions } from '@tthr/client';
|
|
103
|
+
export declare function useMutation<TArgs, TResult>(mutation: MutationFunction<TArgs, TResult>, options?: UseMutationOptions): MutationState<TArgs, TResult>;
|
|
104
|
+
export type { TetherClientOptions, MutationOptions } from '@tthr/client';
|
|
84
105
|
/**
|
|
85
106
|
* Tether Nuxt/Vue project configuration
|
|
86
107
|
*/
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAA+B,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKtE;;GAEG;AACH,eAAO,MAAM,YAAY;iBACV,GAAG,WAAW,mBAAmB;uBAK3B,mBAAmB;CAGvC,CAAC;AAEF;;GAEG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAKxC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,GAAG,OAAO;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EACrC,KAAK,EAAE,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EACpC,IAAI,CAAC,EAAE,KAAK,GACX,UAAU,CAAC,OAAO,CAAC,CA4CrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,EAAE,OAAO;IAC3C,IAAI,EAAE,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC/B,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,GAAG,OAAO;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAA+B,KAAK,GAAG,EAAE,KAAK,GAAG,EAAE,MAAM,KAAK,CAAC;AACtE,OAAO,EAAE,YAAY,EAAE,KAAK,mBAAmB,EAAE,MAAM,cAAc,CAAC;AAKtE;;GAEG;AACH,eAAO,MAAM,YAAY;iBACV,GAAG,WAAW,mBAAmB;uBAK3B,mBAAmB;CAGvC,CAAC;AAEF;;GAEG;AACH,wBAAgB,SAAS,IAAI,YAAY,CAKxC;AAED;;GAEG;AACH,MAAM,WAAW,UAAU,CAAC,CAAC;IAC3B,IAAI,EAAE,GAAG,CAAC,CAAC,GAAG,SAAS,CAAC,CAAC;IACzB,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,GAAG,OAAO;IAC5D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,QAAQ,CAAC,KAAK,EAAE,OAAO,EACrC,KAAK,EAAE,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,EACpC,IAAI,CAAC,EAAE,KAAK,GACX,UAAU,CAAC,OAAO,CAAC,CA4CrB;AAED;;GAEG;AACH,MAAM,WAAW,aAAa,CAAC,KAAK,EAAE,OAAO;IAC3C,IAAI,EAAE,GAAG,CAAC,OAAO,GAAG,SAAS,CAAC,CAAC;IAC/B,KAAK,EAAE,GAAG,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC;IACzB,SAAS,EAAE,GAAG,CAAC,OAAO,CAAC,CAAC;IACxB,MAAM,EAAE,CAAC,IAAI,EAAE,KAAK,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC1C,KAAK,EAAE,MAAM,IAAI,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,gBAAgB,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,GAAG,OAAO;IAC/D,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,KAAK,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;CACxB;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,OAAO,EACxC,QAAQ,EAAE,gBAAgB,CAAC,KAAK,EAAE,OAAO,CAAC,EAC1C,OAAO,CAAC,EAAE,kBAAkB,GAC3B,aAAa,CAAC,KAAK,EAAE,OAAO,CAAC,CAqC/B;AAGD,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC;AAEzE;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,uCAAuC;IACvC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,8CAA8C;IAC9C,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,0BAA0B;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,sCAAsC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY,CAE/D"}
|
package/dist/index.js
CHANGED
|
@@ -92,10 +92,15 @@ export function useQuery(query, args) {
|
|
|
92
92
|
*
|
|
93
93
|
* const createPost = useMutation(api.posts.create);
|
|
94
94
|
* await createPost.mutate({ title: 'Hello', content: '...' });
|
|
95
|
+
*
|
|
96
|
+
* // With query invalidation - refreshes posts list when a comment is added
|
|
97
|
+
* const createComment = useMutation(api.comments.create, {
|
|
98
|
+
* invalidates: ['posts.list']
|
|
99
|
+
* });
|
|
95
100
|
* </script>
|
|
96
101
|
* ```
|
|
97
102
|
*/
|
|
98
|
-
export function useMutation(mutation) {
|
|
103
|
+
export function useMutation(mutation, options) {
|
|
99
104
|
const data = ref();
|
|
100
105
|
const error = ref(null);
|
|
101
106
|
const isPending = ref(false);
|
|
@@ -104,7 +109,9 @@ export function useMutation(mutation) {
|
|
|
104
109
|
isPending.value = true;
|
|
105
110
|
error.value = null;
|
|
106
111
|
const client = useTether();
|
|
107
|
-
const result = await client.mutation(mutation._name, args
|
|
112
|
+
const result = await client.mutation(mutation._name, args, {
|
|
113
|
+
invalidates: options?.invalidates,
|
|
114
|
+
});
|
|
108
115
|
data.value = result;
|
|
109
116
|
return result;
|
|
110
117
|
}
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,EAAE,GAAG,EAAE,SAAS,EAAE,WAAW,EAAsB,MAAM,KAAK,CAAC;AACtE,OAAO,EAAE,YAAY,EAA4B,MAAM,cAAc,CAAC;AAEtE,yBAAyB;AACzB,IAAI,YAAY,GAAwB,IAAI,CAAC;AAE7C;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG;IAC1B,OAAO,CAAC,GAAQ,EAAE,OAA4B;QAC5C,YAAY,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;QACzC,GAAG,CAAC,OAAO,CAAC,QAAQ,EAAE,YAAY,CAAC,CAAC;IACtC,CAAC;IAED,SAAS,CAAC,OAA4B;QACpC,YAAY,GAAG,IAAI,YAAY,CAAC,OAAO,CAAC,CAAC;IAC3C,CAAC;CACF,CAAC;AAEF;;GAEG;AACH,MAAM,UAAU,SAAS;IACvB,IAAI,CAAC,YAAY,EAAE,CAAC;QAClB,MAAM,IAAI,KAAK,CAAC,qEAAqE,CAAC,CAAC;IACzF,CAAC;IACD,OAAO,YAAY,CAAC;AACtB,CAAC;AAqBD;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,QAAQ,CACtB,KAAoC,EACpC,IAAY;IAEZ,MAAM,IAAI,GAAG,GAAG,EAAW,CAAC;IAC5B,MAAM,KAAK,GAAG,GAAG,CAAe,IAAI,CAAC,CAAC;IACtC,MAAM,SAAS,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC;IAE5B,IAAI,WAAW,GAAwB,IAAI,CAAC;IAE5C,MAAM,SAAS,GAAG,KAAK,IAAI,EAAE;QAC3B,IAAI,CAAC;YACH,SAAS,CAAC,KAAK,GAAG,IAAI,CAAC;YACvB,KAAK,CAAC,KAAK,GAAG,IAAI,CAAC;YAEnB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;YAC3B,MAAM,MAAM,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YACrD,IAAI,CAAC,KAAK,GAAG,MAAiB,CAAC;QACjC,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,KAAK,CAAC,KAAK,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC9D,CAAC;gBAAS,CAAC;YACT,SAAS,CAAC,KAAK,GAAG,KAAK,CAAC;QAC1B,CAAC;IACH,CAAC,CAAC;IAEF,SAAS,CAAC,KAAK,IAAI,EAAE;QACnB,MAAM,SAAS,EAAE,CAAC;QAElB,uBAAuB;QACvB,MAAM,MAAM,GAAG,SAAS,EAAE,CAAC;QAC3B,WAAW,GAAG,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,KAAK,EAAE,IAAI,EAAE,CAAC,OAAO,EAAE,EAAE;YAC5D,IAAI,CAAC,KAAK,GAAG,OAAkB,CAAC;QAClC,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,WAAW,CAAC,GAAG,EAAE;QACf,IAAI,WAAW,EAAE,CAAC;YAChB,WAAW,EAAE,CAAC;QAChB,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,IAAI,EAAE,IAAgC;QACtC,KAAK;QACL,SAAS;QACT,OAAO,EAAE,SAAS;KACnB,CAAC;AACJ,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -53,21 +53,84 @@
|
|
|
53
53
|
v-for="post in posts.data.value"
|
|
54
54
|
:key="post.id"
|
|
55
55
|
class="tether-welcome__post"
|
|
56
|
+
:class="{ 'tether-welcome__post--expanded': expandedPostId === post.id }"
|
|
56
57
|
>
|
|
57
|
-
<div class="tether-welcome__post-
|
|
58
|
-
<
|
|
59
|
-
|
|
58
|
+
<div class="tether-welcome__post-header">
|
|
59
|
+
<div class="tether-welcome__post-content">
|
|
60
|
+
<h3>{{ post.title }}</h3>
|
|
61
|
+
<time>{{ formatDate(post.createdAt) }}</time>
|
|
62
|
+
</div>
|
|
63
|
+
<div class="tether-welcome__post-actions">
|
|
64
|
+
<button
|
|
65
|
+
class="tether-welcome__post-comment-toggle"
|
|
66
|
+
@click="toggleComments(post.id)"
|
|
67
|
+
title="Toggle comments"
|
|
68
|
+
>
|
|
69
|
+
<svg viewBox="0 0 20 20" fill="currentColor">
|
|
70
|
+
<path fill-rule="evenodd" d="M18 10c0 3.866-3.582 7-8 7a8.841 8.841 0 01-4.083-.98L2 17l1.338-3.123C2.493 12.767 2 11.434 2 10c0-3.866 3.582-7 8-7s8 3.134 8 7zM7 9H5v2h2V9zm8 0h-2v2h2V9zM9 9h2v2H9V9z" clip-rule="evenodd" />
|
|
71
|
+
</svg>
|
|
72
|
+
<span v-if="getCommentCount(post.id) > 0">{{ getCommentCount(post.id) }}</span>
|
|
73
|
+
</button>
|
|
74
|
+
<button
|
|
75
|
+
class="tether-welcome__post-delete"
|
|
76
|
+
@click="handleDeletePost(post.id)"
|
|
77
|
+
:disabled="deletePost.isPending.value"
|
|
78
|
+
title="Delete post"
|
|
79
|
+
>
|
|
80
|
+
<svg viewBox="0 0 20 20" fill="currentColor">
|
|
81
|
+
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
82
|
+
</svg>
|
|
83
|
+
</button>
|
|
84
|
+
</div>
|
|
85
|
+
</div>
|
|
86
|
+
|
|
87
|
+
<!-- Comments section -->
|
|
88
|
+
<div v-if="expandedPostId === post.id" class="tether-welcome__comments">
|
|
89
|
+
<form class="tether-welcome__comment-form" @submit.prevent="handleCreateComment(post.id)">
|
|
90
|
+
<input
|
|
91
|
+
v-model="newCommentText"
|
|
92
|
+
type="text"
|
|
93
|
+
placeholder="Add a comment..."
|
|
94
|
+
class="tether-welcome__comment-input"
|
|
95
|
+
:disabled="createComment.isPending.value"
|
|
96
|
+
/>
|
|
97
|
+
<button
|
|
98
|
+
type="submit"
|
|
99
|
+
class="tether-welcome__comment-submit"
|
|
100
|
+
:disabled="!newCommentText.trim() || createComment.isPending.value"
|
|
101
|
+
>
|
|
102
|
+
<svg viewBox="0 0 20 20" fill="currentColor">
|
|
103
|
+
<path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 1.414L10.586 9H7a1 1 0 100 2h3.586l-1.293 1.293a1 1 0 101.414 1.414l3-3a1 1 0 000-1.414z" clip-rule="evenodd" />
|
|
104
|
+
</svg>
|
|
105
|
+
</button>
|
|
106
|
+
</form>
|
|
107
|
+
|
|
108
|
+
<div class="tether-welcome__comment-list">
|
|
109
|
+
<div
|
|
110
|
+
v-for="comment in postComments[post.id] || []"
|
|
111
|
+
:key="comment.id"
|
|
112
|
+
class="tether-welcome__comment"
|
|
113
|
+
>
|
|
114
|
+
<p>{{ comment.content }}</p>
|
|
115
|
+
<div class="tether-welcome__comment-meta">
|
|
116
|
+
<time>{{ formatDate(comment.createdAt) }}</time>
|
|
117
|
+
<button
|
|
118
|
+
class="tether-welcome__comment-delete"
|
|
119
|
+
@click="handleDeleteComment(comment.id, post.id)"
|
|
120
|
+
:disabled="deleteComment.isPending.value"
|
|
121
|
+
title="Delete comment"
|
|
122
|
+
>
|
|
123
|
+
<svg viewBox="0 0 20 20" fill="currentColor">
|
|
124
|
+
<path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
125
|
+
</svg>
|
|
126
|
+
</button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<p v-if="!postComments[post.id]?.length" class="tether-welcome__no-comments">
|
|
130
|
+
No comments yet
|
|
131
|
+
</p>
|
|
132
|
+
</div>
|
|
60
133
|
</div>
|
|
61
|
-
<button
|
|
62
|
-
class="tether-welcome__post-delete"
|
|
63
|
-
@click="handleDeletePost(post.id)"
|
|
64
|
-
:disabled="deletePost.isPending.value"
|
|
65
|
-
title="Delete post"
|
|
66
|
-
>
|
|
67
|
-
<svg viewBox="0 0 20 20" fill="currentColor">
|
|
68
|
-
<path fill-rule="evenodd" d="M9 2a1 1 0 00-.894.553L7.382 4H4a1 1 0 000 2v10a2 2 0 002 2h8a2 2 0 002-2V6a1 1 0 100-2h-3.382l-.724-1.447A1 1 0 0011 2H9zM7 8a1 1 0 012 0v6a1 1 0 11-2 0V8zm5-1a1 1 0 00-1 1v6a1 1 0 102 0V8a1 1 0 00-1-1z" clip-rule="evenodd" />
|
|
69
|
-
</svg>
|
|
70
|
-
</button>
|
|
71
134
|
</article>
|
|
72
135
|
</template>
|
|
73
136
|
<p v-else class="tether-welcome__empty">
|
|
@@ -117,18 +180,116 @@ interface Post {
|
|
|
117
180
|
updatedAt: string;
|
|
118
181
|
}
|
|
119
182
|
|
|
183
|
+
interface Comment {
|
|
184
|
+
id: string;
|
|
185
|
+
postId: string;
|
|
186
|
+
content: string;
|
|
187
|
+
authorId: string;
|
|
188
|
+
createdAt: string;
|
|
189
|
+
}
|
|
190
|
+
|
|
120
191
|
const newPostTitle = ref('');
|
|
192
|
+
const expandedPostId = ref<string | null>(null);
|
|
193
|
+
const newCommentText = ref('');
|
|
194
|
+
const postComments = ref<Record<string, Comment[]>>({});
|
|
121
195
|
|
|
122
196
|
// Set up queries and mutations (these use SSR-safe server routes)
|
|
123
197
|
const posts = useQuery<void, Post[]>('posts.list');
|
|
124
198
|
const createPost = useMutation<{ title: string; content?: string }, { id: string }>('posts.create');
|
|
125
199
|
const deletePost = useMutation<{ id: string }, void>('posts.remove');
|
|
200
|
+
const createComment = useMutation<{ postId: string; content: string }, { id: string }>('comments.create');
|
|
201
|
+
const deleteComment = useMutation<{ id: string }, void>('comments.remove');
|
|
126
202
|
|
|
127
203
|
// Set up WebSocket subscription for realtime updates (client-side only)
|
|
128
|
-
|
|
129
|
-
|
|
204
|
+
// Uses setData to hot-swap data without full reload
|
|
205
|
+
const { isConnected } = useTetherSubscription('posts.list', undefined, (data) => {
|
|
206
|
+
if (data && Array.isArray(data)) {
|
|
207
|
+
posts.setData(data as Post[]);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
// Subscribe to comments updates
|
|
212
|
+
useTetherSubscription('comments.list', undefined, (data) => {
|
|
213
|
+
if (data && Array.isArray(data)) {
|
|
214
|
+
// Group comments by postId
|
|
215
|
+
const grouped: Record<string, Comment[]> = {};
|
|
216
|
+
for (const comment of data as Comment[]) {
|
|
217
|
+
if (!grouped[comment.postId]) {
|
|
218
|
+
grouped[comment.postId] = [];
|
|
219
|
+
}
|
|
220
|
+
grouped[comment.postId].push(comment);
|
|
221
|
+
}
|
|
222
|
+
postComments.value = grouped;
|
|
223
|
+
}
|
|
130
224
|
});
|
|
131
225
|
|
|
226
|
+
async function toggleComments(postId: string) {
|
|
227
|
+
if (expandedPostId.value === postId) {
|
|
228
|
+
expandedPostId.value = null;
|
|
229
|
+
} else {
|
|
230
|
+
expandedPostId.value = postId;
|
|
231
|
+
// Load comments for this post if not already loaded
|
|
232
|
+
if (!postComments.value[postId]) {
|
|
233
|
+
try {
|
|
234
|
+
const response = await $fetch<{ data: Comment[] }>('/api/_tether/query', {
|
|
235
|
+
method: 'POST',
|
|
236
|
+
body: {
|
|
237
|
+
function: 'comments.listByPost',
|
|
238
|
+
args: { postId },
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
postComments.value[postId] = response.data || [];
|
|
242
|
+
} catch {
|
|
243
|
+
postComments.value[postId] = [];
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
async function handleCreateComment(postId: string) {
|
|
250
|
+
if (!newCommentText.value.trim()) return;
|
|
251
|
+
|
|
252
|
+
try {
|
|
253
|
+
await createComment.mutate({
|
|
254
|
+
postId,
|
|
255
|
+
content: newCommentText.value.trim(),
|
|
256
|
+
});
|
|
257
|
+
newCommentText.value = '';
|
|
258
|
+
// Refresh comments for this post
|
|
259
|
+
const response = await $fetch<{ data: Comment[] }>('/api/_tether/query', {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
body: {
|
|
262
|
+
function: 'comments.listByPost',
|
|
263
|
+
args: { postId },
|
|
264
|
+
},
|
|
265
|
+
});
|
|
266
|
+
postComments.value[postId] = response.data || [];
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error('Failed to create comment:', error);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
async function handleDeleteComment(commentId: string, postId: string) {
|
|
273
|
+
try {
|
|
274
|
+
await deleteComment.mutate({ id: commentId });
|
|
275
|
+
// Refresh comments for this post
|
|
276
|
+
const response = await $fetch<{ data: Comment[] }>('/api/_tether/query', {
|
|
277
|
+
method: 'POST',
|
|
278
|
+
body: {
|
|
279
|
+
function: 'comments.listByPost',
|
|
280
|
+
args: { postId },
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
postComments.value[postId] = response.data || [];
|
|
284
|
+
} catch (error) {
|
|
285
|
+
console.error('Failed to delete comment:', error);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function getCommentCount(postId: string): number {
|
|
290
|
+
return postComments.value[postId]?.length || 0;
|
|
291
|
+
}
|
|
292
|
+
|
|
132
293
|
async function handleCreatePost() {
|
|
133
294
|
if (!newPostTitle.value.trim()) return;
|
|
134
295
|
|
|
@@ -165,9 +326,15 @@ function formatDate(dateString: string): string {
|
|
|
165
326
|
}
|
|
166
327
|
</script>
|
|
167
328
|
|
|
329
|
+
<style>
|
|
330
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
331
|
+
body { padding: 0; margin: 0; height: 100dvh; }
|
|
332
|
+
#__nuxt { height: 100dvh; }
|
|
333
|
+
</style>
|
|
334
|
+
|
|
168
335
|
<style scoped>
|
|
169
336
|
.tether-welcome {
|
|
170
|
-
min-height:
|
|
337
|
+
min-height: 100dvh;
|
|
171
338
|
background: linear-gradient(135deg, #0f0f23 0%, #1a1a2e 50%, #16213e 100%);
|
|
172
339
|
color: #e4e4e7;
|
|
173
340
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
@@ -200,10 +367,7 @@ function formatDate(dateString: string): string {
|
|
|
200
367
|
font-size: 2.5rem;
|
|
201
368
|
font-weight: 700;
|
|
202
369
|
margin: 0;
|
|
203
|
-
|
|
204
|
-
-webkit-background-clip: text;
|
|
205
|
-
-webkit-text-fill-color: transparent;
|
|
206
|
-
background-clip: text;
|
|
370
|
+
color: #f4f4f5;
|
|
207
371
|
}
|
|
208
372
|
|
|
209
373
|
.tether-welcome__subtitle {
|
|
@@ -342,20 +506,32 @@ function formatDate(dateString: string): string {
|
|
|
342
506
|
|
|
343
507
|
.tether-welcome__post {
|
|
344
508
|
display: flex;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
padding: 0.875rem 1rem;
|
|
509
|
+
flex-direction: column;
|
|
510
|
+
gap: 0;
|
|
511
|
+
padding: 0;
|
|
349
512
|
background: rgba(0, 0, 0, 0.2);
|
|
350
513
|
border: 1px solid rgba(255, 255, 255, 0.05);
|
|
351
514
|
border-radius: 0.5rem;
|
|
352
515
|
transition: background 0.2s;
|
|
516
|
+
overflow: hidden;
|
|
353
517
|
}
|
|
354
518
|
|
|
355
519
|
.tether-welcome__post:hover {
|
|
356
520
|
background: rgba(0, 0, 0, 0.3);
|
|
357
521
|
}
|
|
358
522
|
|
|
523
|
+
.tether-welcome__post--expanded {
|
|
524
|
+
background: rgba(0, 0, 0, 0.3);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
.tether-welcome__post-header {
|
|
528
|
+
display: flex;
|
|
529
|
+
align-items: center;
|
|
530
|
+
justify-content: space-between;
|
|
531
|
+
gap: 1rem;
|
|
532
|
+
padding: 0.875rem 1rem;
|
|
533
|
+
}
|
|
534
|
+
|
|
359
535
|
.tether-welcome__post-content {
|
|
360
536
|
flex: 1;
|
|
361
537
|
min-width: 0;
|
|
@@ -376,6 +552,36 @@ function formatDate(dateString: string): string {
|
|
|
376
552
|
color: #71717a;
|
|
377
553
|
}
|
|
378
554
|
|
|
555
|
+
.tether-welcome__post-actions {
|
|
556
|
+
display: flex;
|
|
557
|
+
gap: 0.25rem;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
.tether-welcome__post-comment-toggle {
|
|
561
|
+
flex-shrink: 0;
|
|
562
|
+
display: flex;
|
|
563
|
+
align-items: center;
|
|
564
|
+
gap: 0.25rem;
|
|
565
|
+
padding: 0.375rem 0.5rem;
|
|
566
|
+
background: transparent;
|
|
567
|
+
border: none;
|
|
568
|
+
border-radius: 0.375rem;
|
|
569
|
+
color: #71717a;
|
|
570
|
+
font-size: 0.75rem;
|
|
571
|
+
cursor: pointer;
|
|
572
|
+
transition: color 0.2s, background 0.2s;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
.tether-welcome__post-comment-toggle:hover {
|
|
576
|
+
color: #a1a1aa;
|
|
577
|
+
background: rgba(255, 255, 255, 0.05);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.tether-welcome__post-comment-toggle svg {
|
|
581
|
+
width: 16px;
|
|
582
|
+
height: 16px;
|
|
583
|
+
}
|
|
584
|
+
|
|
379
585
|
.tether-welcome__post-delete {
|
|
380
586
|
flex-shrink: 0;
|
|
381
587
|
width: 32px;
|
|
@@ -406,6 +612,133 @@ function formatDate(dateString: string): string {
|
|
|
406
612
|
height: 18px;
|
|
407
613
|
}
|
|
408
614
|
|
|
615
|
+
/* Comments styles */
|
|
616
|
+
.tether-welcome__comments {
|
|
617
|
+
padding: 0.75rem 1rem 1rem;
|
|
618
|
+
border-top: 1px solid rgba(255, 255, 255, 0.05);
|
|
619
|
+
background: rgba(0, 0, 0, 0.15);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.tether-welcome__comment-form {
|
|
623
|
+
display: flex;
|
|
624
|
+
gap: 0.5rem;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
.tether-welcome__comment-input {
|
|
628
|
+
flex: 1;
|
|
629
|
+
padding: 0.5rem 0.75rem;
|
|
630
|
+
background: rgba(0, 0, 0, 0.3);
|
|
631
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
632
|
+
border-radius: 0.375rem;
|
|
633
|
+
color: #e4e4e7;
|
|
634
|
+
font-size: 0.8125rem;
|
|
635
|
+
outline: none;
|
|
636
|
+
transition: border-color 0.2s, box-shadow 0.2s;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
.tether-welcome__comment-input:focus {
|
|
640
|
+
border-color: #6366f1;
|
|
641
|
+
box-shadow: 0 0 0 2px rgba(99, 102, 241, 0.2);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
.tether-welcome__comment-input::placeholder {
|
|
645
|
+
color: #71717a;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
.tether-welcome__comment-submit {
|
|
649
|
+
display: flex;
|
|
650
|
+
align-items: center;
|
|
651
|
+
justify-content: center;
|
|
652
|
+
width: 32px;
|
|
653
|
+
height: 32px;
|
|
654
|
+
background: #6366f1;
|
|
655
|
+
border: none;
|
|
656
|
+
border-radius: 0.375rem;
|
|
657
|
+
color: white;
|
|
658
|
+
cursor: pointer;
|
|
659
|
+
transition: opacity 0.2s;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
.tether-welcome__comment-submit:hover:not(:disabled) {
|
|
663
|
+
opacity: 0.9;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
.tether-welcome__comment-submit:disabled {
|
|
667
|
+
opacity: 0.5;
|
|
668
|
+
cursor: not-allowed;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
.tether-welcome__comment-submit svg {
|
|
672
|
+
width: 16px;
|
|
673
|
+
height: 16px;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
.tether-welcome__comment-list {
|
|
677
|
+
display: flex;
|
|
678
|
+
flex-direction: column;
|
|
679
|
+
gap: 0.5rem;
|
|
680
|
+
padding-top: 0.75rem;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
.tether-welcome__comment {
|
|
684
|
+
padding: 0.5rem 0.75rem;
|
|
685
|
+
background: rgba(255, 255, 255, 0.03);
|
|
686
|
+
border-radius: 0.375rem;
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
.tether-welcome__comment p {
|
|
690
|
+
margin: 0 0 0.25rem 0;
|
|
691
|
+
font-size: 0.8125rem;
|
|
692
|
+
color: #e4e4e7;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
.tether-welcome__comment-meta {
|
|
696
|
+
display: flex;
|
|
697
|
+
align-items: center;
|
|
698
|
+
justify-content: space-between;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
.tether-welcome__comment-meta time {
|
|
702
|
+
font-size: 0.6875rem;
|
|
703
|
+
color: #71717a;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
.tether-welcome__comment-delete {
|
|
707
|
+
display: flex;
|
|
708
|
+
align-items: center;
|
|
709
|
+
justify-content: center;
|
|
710
|
+
width: 20px;
|
|
711
|
+
height: 20px;
|
|
712
|
+
background: transparent;
|
|
713
|
+
border: none;
|
|
714
|
+
border-radius: 0.25rem;
|
|
715
|
+
color: #71717a;
|
|
716
|
+
cursor: pointer;
|
|
717
|
+
transition: color 0.2s;
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
.tether-welcome__comment-delete:hover:not(:disabled) {
|
|
721
|
+
color: #ef4444;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
.tether-welcome__comment-delete:disabled {
|
|
725
|
+
opacity: 0.5;
|
|
726
|
+
cursor: not-allowed;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
.tether-welcome__comment-delete svg {
|
|
730
|
+
width: 12px;
|
|
731
|
+
height: 12px;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
.tether-welcome__no-comments {
|
|
735
|
+
margin: 0;
|
|
736
|
+
padding: 0.5rem;
|
|
737
|
+
text-align: center;
|
|
738
|
+
font-size: 0.75rem;
|
|
739
|
+
color: #71717a;
|
|
740
|
+
}
|
|
741
|
+
|
|
409
742
|
.tether-welcome__links {
|
|
410
743
|
display: flex;
|
|
411
744
|
justify-content: center;
|
|
@@ -16,6 +16,8 @@ export interface QueryState<T> {
|
|
|
16
16
|
error: Ref<Error | null>;
|
|
17
17
|
isLoading: Ref<boolean>;
|
|
18
18
|
refetch: () => Promise<void>;
|
|
19
|
+
/** Update data directly (used for hot-swapping from WebSocket) */
|
|
20
|
+
setData: (newData: T) => void;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/**
|
|
@@ -77,11 +79,17 @@ export function useQuery<TArgs, TResult>(
|
|
|
77
79
|
fetchData();
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
// Direct data setter for hot-swapping from WebSocket
|
|
83
|
+
const setData = (newData: TResult) => {
|
|
84
|
+
data.value = newData;
|
|
85
|
+
};
|
|
86
|
+
|
|
80
87
|
return {
|
|
81
88
|
data: data as Ref<TResult | undefined>,
|
|
82
89
|
error,
|
|
83
90
|
isLoading,
|
|
84
91
|
refetch: fetchData,
|
|
92
|
+
setData,
|
|
85
93
|
};
|
|
86
94
|
}
|
|
87
95
|
|
|
@@ -180,13 +188,15 @@ export function useMutation<TArgs, TResult>(
|
|
|
180
188
|
export function useTetherSubscription(
|
|
181
189
|
queryName: string,
|
|
182
190
|
args: Record<string, unknown> | undefined,
|
|
183
|
-
onUpdate: () => void
|
|
191
|
+
onUpdate: (data?: unknown) => void
|
|
184
192
|
): { isConnected: Ref<boolean> } {
|
|
185
193
|
const isConnected = ref(false);
|
|
186
194
|
|
|
187
195
|
if (import.meta.client) {
|
|
188
196
|
let ws: WebSocket | null = null;
|
|
189
197
|
let reconnectTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
198
|
+
// Generate a unique subscription ID
|
|
199
|
+
const subscriptionId = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
190
200
|
|
|
191
201
|
const connect = () => {
|
|
192
202
|
// Get config from window (set by plugin)
|
|
@@ -200,20 +210,25 @@ export function useTetherSubscription(
|
|
|
200
210
|
ws = new WebSocket(wsUrl);
|
|
201
211
|
|
|
202
212
|
ws.onopen = () => {
|
|
203
|
-
|
|
204
|
-
// Subscribe to the query
|
|
205
|
-
ws?.send(JSON.stringify({
|
|
206
|
-
type: 'subscribe',
|
|
207
|
-
query: queryName,
|
|
208
|
-
args,
|
|
209
|
-
}));
|
|
213
|
+
// Wait for connected message before subscribing
|
|
210
214
|
};
|
|
211
215
|
|
|
212
216
|
ws.onmessage = (event) => {
|
|
213
217
|
try {
|
|
214
218
|
const message = JSON.parse(event.data);
|
|
215
|
-
|
|
216
|
-
|
|
219
|
+
|
|
220
|
+
if (message.type === 'connected') {
|
|
221
|
+
isConnected.value = true;
|
|
222
|
+
// Subscribe to the query with our ID
|
|
223
|
+
ws?.send(JSON.stringify({
|
|
224
|
+
type: 'subscribe',
|
|
225
|
+
id: subscriptionId,
|
|
226
|
+
query: queryName,
|
|
227
|
+
args,
|
|
228
|
+
}));
|
|
229
|
+
} else if (message.type === 'data' && message.id === subscriptionId) {
|
|
230
|
+
// Call onUpdate with the fresh data
|
|
231
|
+
onUpdate(message.data);
|
|
217
232
|
}
|
|
218
233
|
} catch {
|
|
219
234
|
// Ignore parse errors
|