Skip to content

Commit 7673033

Browse files
committed
Create examples/nuxt with Claude Opus 4.6
1 parent b9c7b5e commit 7673033

29 files changed

Lines changed: 1913 additions & 104 deletions

examples/nuxt/.gitignore

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
node_modules
2+
dist
3+
.output
4+
.nuxt
5+
.data

examples/nuxt/README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
<!-- deno-fmt-ignore-file -->
2+
3+
Fedify-Nuxt integration example application
4+
===========================================
5+
6+
A comprehensive example of building a federated server application using
7+
[Fedify] with [Nuxt]. This example demonstrates how to create an
8+
ActivityPub-compatible federated social media server that can interact with
9+
other federated platforms like [Mastodon], [Misskey], and other ActivityPub
10+
implementations using the Fedify and [Nuxt].
11+
12+
[Fedify]: https://fedify.dev
13+
[Nuxt]: https://nuxt.com/
14+
[Mastodon]: https://mastodon.social/
15+
[Misskey]: https://misskey.io/
16+
17+
18+
Running the example
19+
-------------------
20+
21+
~~~~ sh
22+
pnpm dev
23+
~~~~
24+
25+
26+
Communicate with other federated servers
27+
----------------------------------------
28+
29+
1. Tunnel your local server to the internet using `fedify tunnel`
30+
31+
~~~~ sh
32+
fedify tunnel 3000
33+
~~~~
34+
35+
2. Open the tunneled URL in your browser and check that the server is running
36+
properly.
37+
38+
3. Search your handle and follow from other federated servers such as
39+
[Mastodon] or [Misskey].
40+
41+
> [!NOTE]
42+
> [ActivityPub Academy] is a great resource to learn how to interact
43+
> with other federated servers using ActivityPub protocol.
44+
45+
[ActivityPub Academy]: https://www.activitypub.academy/

examples/nuxt/app.vue

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
<template>
2+
<NuxtPage />
3+
</template>
4+
5+
<script setup lang="ts">
6+
useHead({
7+
link: [
8+
{ rel: "stylesheet", href: "/style.css" },
9+
{ rel: "icon", type: "image/svg+xml", href: "/fedify-logo.svg" },
10+
],
11+
script: [{ src: "/theme.js" }],
12+
});
13+
</script>

examples/nuxt/nuxt.config.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export default defineNuxtConfig({
2+
modules: ["@fedify/nuxt"],
3+
fedify: {
4+
federationModule: "~/server/federation",
5+
},
6+
ssr: true,
7+
devServer: { host: "0.0.0.0" },
8+
vite: { server: { allowedHosts: true } },
9+
});

examples/nuxt/package.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "nuxt-example",
3+
"version": "0.0.1",
4+
"private": true,
5+
"type": "module",
6+
"description": "Fedify app with Nuxt integration",
7+
"scripts": {
8+
"dev": "nuxt dev",
9+
"build": "nuxt build",
10+
"preview": "nuxt preview",
11+
"start": "node .output/server/index.mjs"
12+
},
13+
"dependencies": {
14+
"@fedify/fedify": "workspace:^",
15+
"@fedify/nuxt": "workspace:^",
16+
"@fedify/vocab": "workspace:^",
17+
"@logtape/logtape": "catalog:",
18+
"nuxt": "catalog:",
19+
"h3": "catalog:",
20+
"vue": "^3.5.13",
21+
"x-forwarded-fetch": "^0.2.0"
22+
},
23+
"devDependencies": {
24+
"typescript": "catalog:",
25+
"@types/node": "catalog:"
26+
}
27+
}

examples/nuxt/pages/index.vue

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
<template>
2+
<div class="home-container">
3+
<div class="profile-header">
4+
<div class="avatar-section">
5+
<img
6+
src="/demo-profile.png"
7+
alt="Fedify Demo profile"
8+
class="avatar"
9+
/>
10+
</div>
11+
<div class="user-info">
12+
<h1 class="user-name">Fedify Demo</h1>
13+
<p class="user-handle" v-if="data">
14+
@{{ data.identifier }}@{{ data.host }}
15+
</p>
16+
<p class="user-bio">This is a Fedify Demo account.</p>
17+
</div>
18+
</div>
19+
20+
<!-- Search -->
21+
<div class="info-card">
22+
<h3>Search</h3>
23+
<input
24+
v-model="searchQuery"
25+
type="text"
26+
class="search-input"
27+
placeholder="Search by handle (e.g. @user@example.com)"
28+
@input="onSearchInput"
29+
/>
30+
<div v-if="searchResult" class="search-result">
31+
<img
32+
:src="searchResult.icon ?? '/demo-profile.png'"
33+
:alt="searchResult.name ?? 'User'"
34+
class="post-avatar"
35+
/>
36+
<div class="post-user-info">
37+
<span class="post-user-name">{{ searchResult.name }}</span>
38+
<span class="post-user-handle">{{ searchResult.handle }}</span>
39+
</div>
40+
<form
41+
v-if="searchResult.isFollowing"
42+
method="post"
43+
action="/api/unfollow"
44+
>
45+
<input type="hidden" name="uri" :value="searchResult.uri" />
46+
<button type="submit" class="danger-button">Unfollow</button>
47+
</form>
48+
<form v-else method="post" action="/api/follow">
49+
<input type="hidden" name="uri" :value="searchResult.uri" />
50+
<button type="submit" class="post-button">Follow</button>
51+
</form>
52+
</div>
53+
</div>
54+
55+
<!-- Following -->
56+
<div class="info-card" id="following-section">
57+
<h3>Following ({{ data?.following.length ?? 0 }})</h3>
58+
<div class="info-grid" v-if="data && data.following.length > 0">
59+
<div
60+
v-for="f in data.following"
61+
:key="f.uri"
62+
class="info-item follower-row"
63+
>
64+
<img
65+
:src="f.icon ?? '/demo-profile.png'"
66+
:alt="f.name ?? 'User'"
67+
class="post-avatar"
68+
/>
69+
<div class="post-user-info">
70+
<span class="post-user-name">{{ f.name }}</span>
71+
<span class="follower-item">{{ f.handle }}</span>
72+
</div>
73+
<form method="post" action="/api/unfollow">
74+
<input type="hidden" name="uri" :value="f.uri" />
75+
<button type="submit" class="danger-button">Unfollow</button>
76+
</form>
77+
</div>
78+
</div>
79+
<p v-else>Not following anyone yet.</p>
80+
</div>
81+
82+
<!-- Followers -->
83+
<div class="info-card" id="followers-section">
84+
<h3>Followers ({{ data?.followers.length ?? 0 }})</h3>
85+
<div class="info-grid" v-if="data && data.followers.length > 0">
86+
<div
87+
v-for="f in data.followers"
88+
:key="f.uri"
89+
class="info-item follower-row"
90+
>
91+
<img
92+
:src="f.icon ?? '/demo-profile.png'"
93+
:alt="f.name ?? 'User'"
94+
class="post-avatar"
95+
/>
96+
<div class="post-user-info">
97+
<span class="post-user-name">{{ f.name }}</span>
98+
<span class="follower-item">{{ f.handle }}</span>
99+
</div>
100+
</div>
101+
</div>
102+
<p v-else>
103+
No followers yet. Try following this account from another fediverse
104+
server.
105+
</p>
106+
</div>
107+
108+
<!-- Compose -->
109+
<div class="info-card">
110+
<h3>Compose</h3>
111+
<form method="post" action="/api/post" class="compose-form">
112+
<textarea
113+
name="content"
114+
class="form-textarea"
115+
placeholder="What's up?"
116+
rows="3"
117+
></textarea>
118+
<div class="compose-actions">
119+
<button type="submit" class="post-button">Post</button>
120+
</div>
121+
</form>
122+
</div>
123+
124+
<!-- Posts -->
125+
<div class="info-card">
126+
<h3>Posts</h3>
127+
<div
128+
class="posts-grid"
129+
v-if="data && data.posts.length > 0"
130+
>
131+
<article v-for="post in data.posts" :key="post.id" class="post-card">
132+
<a :href="post.url" class="post-link">
133+
<div class="post-header">
134+
<img
135+
src="/demo-profile.png"
136+
alt="Fedify Demo"
137+
class="post-avatar"
138+
/>
139+
<div class="post-user-info">
140+
<h3 class="post-user-name">Fedify Demo</h3>
141+
<p class="post-user-handle">
142+
@{{ data.identifier }}@{{ data.host }}
143+
</p>
144+
</div>
145+
</div>
146+
<div class="post-content">
147+
<p>{{ post.content }}</p>
148+
</div>
149+
<div v-if="post.published" class="post-timestamp">
150+
{{ formatDate(post.published) }}
151+
</div>
152+
</a>
153+
</article>
154+
</div>
155+
<p v-else>No posts yet. Write your first post above!</p>
156+
</div>
157+
158+
<div class="fedify-badge">
159+
Powered by
160+
<a href="https://fedify.dev" class="fedify-anchor" target="_blank">
161+
Fedify
162+
</a>
163+
</div>
164+
</div>
165+
</template>
166+
167+
<script setup lang="ts">
168+
useHead({ title: "Fedify Nuxt Example" });
169+
170+
const { data, refresh } = await useFetch("/api/home");
171+
172+
const searchQuery = ref("");
173+
const searchResult = ref<{
174+
uri: string;
175+
name: string | null;
176+
handle: string;
177+
icon: string | null;
178+
isFollowing: boolean;
179+
} | null>(null);
180+
181+
let searchTimeout: ReturnType<typeof setTimeout> | null = null;
182+
183+
function onSearchInput() {
184+
if (searchTimeout) clearTimeout(searchTimeout);
185+
searchTimeout = setTimeout(async () => {
186+
if (!searchQuery.value.trim()) {
187+
searchResult.value = null;
188+
return;
189+
}
190+
const res = await $fetch<{ result: typeof searchResult.value }>(
191+
`/api/search?q=${encodeURIComponent(searchQuery.value)}`,
192+
);
193+
searchResult.value = res.result;
194+
}, 300);
195+
}
196+
197+
function formatDate(dateStr: string): string {
198+
return new Date(dateStr).toLocaleString(undefined, {
199+
year: "numeric",
200+
month: "long",
201+
day: "numeric",
202+
hour: "2-digit",
203+
minute: "2-digit",
204+
});
205+
}
206+
207+
onMounted(() => {
208+
const eventSource = new EventSource("/api/events");
209+
eventSource.onmessage = () => {
210+
refresh();
211+
};
212+
onBeforeUnmount(() => {
213+
eventSource.close();
214+
});
215+
});
216+
</script>
217+
218+
<style scoped>
219+
.search-input {
220+
width: 100%;
221+
border-radius: 0.5rem;
222+
border: 1px solid rgba(0, 0, 0, 0.2);
223+
padding: 0.75rem;
224+
font-size: 1rem;
225+
background: var(--background);
226+
color: var(--foreground);
227+
transition: border-color 0.2s, box-shadow 0.2s;
228+
font-family: inherit;
229+
}
230+
.search-input:focus {
231+
outline: none;
232+
border-color: #3b82f6;
233+
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.3);
234+
}
235+
.search-result {
236+
display: flex;
237+
align-items: center;
238+
gap: 0.75rem;
239+
margin-top: 1rem;
240+
padding: 0.75rem;
241+
border-radius: 0.5rem;
242+
border: 1px solid rgba(0, 0, 0, 0.1);
243+
}
244+
.follower-row {
245+
display: flex;
246+
flex-direction: row;
247+
align-items: center;
248+
gap: 0.75rem;
249+
}
250+
.compose-form {
251+
display: flex;
252+
flex-direction: column;
253+
gap: 0.75rem;
254+
}
255+
.compose-actions {
256+
display: flex;
257+
justify-content: flex-end;
258+
}
259+
.danger-button {
260+
padding: 0.5rem 1.5rem;
261+
border: none;
262+
border-radius: 0.5rem;
263+
font-size: 1rem;
264+
font-weight: 600;
265+
color: white;
266+
cursor: pointer;
267+
background: #ef4444;
268+
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
269+
transition: transform 0.2s, box-shadow 0.2s;
270+
}
271+
.danger-button:hover {
272+
transform: translateY(-1px);
273+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
274+
}
275+
.post-timestamp {
276+
font-size: 0.875rem;
277+
opacity: 0.6;
278+
margin-top: 0.5rem;
279+
padding: 0 1.5rem 1rem;
280+
}
281+
.fedify-badge {
282+
text-align: center;
283+
margin-top: 1rem;
284+
font-size: 0.875rem;
285+
opacity: 0.7;
286+
}
287+
</style>

0 commit comments

Comments
 (0)