Loading examples/sveltekit-sample/src/app.css +146 −0 Original line number Diff line number Diff line Loading @@ -129,4 +129,150 @@ body { @apply flex-col items-start gap-1; } } /* Posts Page Styles */ .post-form { @apply mx-auto my-8 max-w-4xl rounded-xl border p-6; background: var(--background); border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .form-group { @apply mb-4; } .form-label { @apply mb-2 block text-lg font-semibold; color: var(--foreground); } .form-textarea { @apply w-full resize-none rounded-lg border p-3 text-base focus:ring-2 focus:ring-blue-500 focus:outline-none; background: var(--background); color: var(--foreground); border-color: rgba(0, 0, 0, 0.2); transition: border-color 0.2s, box-shadow 0.2s; } .form-textarea:focus { border-color: #3b82f6; } .post-button { @apply rounded-lg px-6 py-2 font-semibold text-white transition-all duration-200; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .post-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .error-state, .loading-state { @apply py-12 text-center; } .error-state p { @apply text-lg text-red-600; } .loading-state p { @apply mt-4 text-gray-600; } .posts-container { @apply mx-auto max-w-4xl; } .posts-title { @apply mb-6 text-2xl font-bold; color: var(--foreground); } .posts-grid { @apply grid gap-6; } .post-card { @apply rounded-xl border transition-all duration-200 hover:shadow-lg; background: var(--background); border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .post-card:hover { transform: translateY(-2px); } .post-link { @apply block p-6 no-underline; color: inherit; } .post-header { @apply mb-4 flex items-center gap-3; } .post-avatar { @apply h-12 w-12 rounded-full border-2 border-gray-200 object-cover; } .post-user-info { @apply flex-1; } .post-user-name { @apply mb-1 text-lg font-semibold; color: var(--foreground); } .post-user-handle { @apply text-sm opacity-70; color: var(--foreground); } .post-content { @apply text-base leading-relaxed; color: var(--foreground); } .post-content p { @apply m-0; } /* Skeleton Styles */ .skeleton-avatar { @apply h-12 w-12 animate-pulse rounded-full bg-gray-300; } .skeleton-info { @apply flex-1 space-y-2; } .skeleton-line { @apply h-4 animate-pulse rounded bg-gray-300; } .skeleton-name { @apply w-24; } .skeleton-handle { @apply w-32; } @media (max-width: 768px) { .posts-container { @apply px-4; } .post-form { @apply p-4; } } } examples/sveltekit-sample/src/lib/components/Spinner.svelte 0 → 100644 +21 −0 Original line number Diff line number Diff line <svg class="mr-3 -ml-1 size-24 animate-spin text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </path> </svg> examples/sveltekit-sample/src/lib/store.ts +5 −8 Original line number Diff line number Diff line Loading @@ -22,14 +22,11 @@ class PostStore { return this.#map.get(id.toString()); } get = this.#get.bind(this); async #getAll() { return await Array.fromAsync( this.#timeline.reverse() #getAll() { return this.#timeline.reverse() .map((id) => id.toString()) .map((id) => this.#map.get(id)!) .filter((p) => p) .map((p) => p.toJsonLd()), ); .filter((p) => p); } getAll = this.#getAll.bind(this); #delete(id: URL) { Loading examples/sveltekit-sample/src/routes/users/[identifier]/+page.svelte +2 −18 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import type { PageProps } from "./$types"; import { browser } from "$app/environment"; import type { Person } from "@fedify/fedify"; import Spinner from "$lib/components/Spinner.svelte"; let { params }: PageProps = $props(); const { identifier } = params; Loading @@ -17,24 +18,7 @@ {#await data} <!-- promise is pending --> <div class="flex h-svh w-svw items-center justify-center"> <svg class="mr-3 -ml-1 size-24 animate-spin text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" ><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" ></path></svg > <Spinner /> </div> {:then user} {#if user} Loading examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.svelte +67 −13 Original line number Diff line number Diff line <script lang="ts"> import type { PageProps } from "./$types"; import { browser } from "$app/environment"; import { getPosts } from "./data.remote"; import Spinner from "$lib/components/Spinner.svelte"; import type { Person } from "@fedify/fedify"; let { params }: PageProps = $props(); const { identifier } = params; const query = getPosts(); const data = browser ? fetch(`/users/${identifier}`, { headers: { Accept: "application/activity+json" }, }).then( (res) => res.json() as Promise<Person & { icon: { url: string } }>, ) : Promise.resolve(null); </script> <form method="POST" action="?/post"> <form method="POST" action="?/post" class="post-form"> <input name="identifier" type="hidden" value={identifier} /> <label> Content <input name="content" type="text" /> <div class="form-group"> <label class="form-label"> 새 포스트 작성 <textarea name="content" class="form-textarea" placeholder="무엇을 생각하고 계신가요?" rows="3" ></textarea> </label> <button>Post</button> </div> <button type="submit" class="post-button">게시하기</button> </form> {#if query.error} <p>oops!</p> <div class="error-state"> <p>포스트를 불러오는 중 오류가 발생했습니다.</p> </div> {:else if query.loading} <p>loading...</p> <div class="loading-state"> <Spinner /> <p>포스트를 불러오는 중...</p> </div> {:else if query.current} <ul> <div class="posts-container"> <h2 class="posts-title">포스트 목록</h2> <div class="posts-grid"> {#each query.current as note} <pre>{JSON.stringify(note, null, 2)}</pre> <article class="post-card"> <a href={note.url} class="post-link"> <div class="post-header"> {#await data} <div class="skeleton-avatar"></div> <div class="skeleton-info"> <div class="skeleton-line skeleton-name"></div> <div class="skeleton-line skeleton-handle"></div> </div> {:then user} {#if user} <img src={user.icon?.url ?? "/demo-profile.png"} alt="{user.name}'s profile" class="post-avatar" /> <div class="post-user-info"> <h3 class="post-user-name">{user.name}</h3> <p class="post-user-handle"> @{identifier}@{window.location.host} </p> </div> {/if} {/await} </div> <div class="post-content"> <p>{note.content}</p> </div> </a> </article> {/each} </ul> </div> </div> {/if} Loading
examples/sveltekit-sample/src/app.css +146 −0 Original line number Diff line number Diff line Loading @@ -129,4 +129,150 @@ body { @apply flex-col items-start gap-1; } } /* Posts Page Styles */ .post-form { @apply mx-auto my-8 max-w-4xl rounded-xl border p-6; background: var(--background); border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.05); } .form-group { @apply mb-4; } .form-label { @apply mb-2 block text-lg font-semibold; color: var(--foreground); } .form-textarea { @apply w-full resize-none rounded-lg border p-3 text-base focus:ring-2 focus:ring-blue-500 focus:outline-none; background: var(--background); color: var(--foreground); border-color: rgba(0, 0, 0, 0.2); transition: border-color 0.2s, box-shadow 0.2s; } .form-textarea:focus { border-color: #3b82f6; } .post-button { @apply rounded-lg px-6 py-2 font-semibold text-white transition-all duration-200; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .post-button:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); } .error-state, .loading-state { @apply py-12 text-center; } .error-state p { @apply text-lg text-red-600; } .loading-state p { @apply mt-4 text-gray-600; } .posts-container { @apply mx-auto max-w-4xl; } .posts-title { @apply mb-6 text-2xl font-bold; color: var(--foreground); } .posts-grid { @apply grid gap-6; } .post-card { @apply rounded-xl border transition-all duration-200 hover:shadow-lg; background: var(--background); border-color: rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); } .post-card:hover { transform: translateY(-2px); } .post-link { @apply block p-6 no-underline; color: inherit; } .post-header { @apply mb-4 flex items-center gap-3; } .post-avatar { @apply h-12 w-12 rounded-full border-2 border-gray-200 object-cover; } .post-user-info { @apply flex-1; } .post-user-name { @apply mb-1 text-lg font-semibold; color: var(--foreground); } .post-user-handle { @apply text-sm opacity-70; color: var(--foreground); } .post-content { @apply text-base leading-relaxed; color: var(--foreground); } .post-content p { @apply m-0; } /* Skeleton Styles */ .skeleton-avatar { @apply h-12 w-12 animate-pulse rounded-full bg-gray-300; } .skeleton-info { @apply flex-1 space-y-2; } .skeleton-line { @apply h-4 animate-pulse rounded bg-gray-300; } .skeleton-name { @apply w-24; } .skeleton-handle { @apply w-32; } @media (max-width: 768px) { .posts-container { @apply px-4; } .post-form { @apply p-4; } } }
examples/sveltekit-sample/src/lib/components/Spinner.svelte 0 → 100644 +21 −0 Original line number Diff line number Diff line <svg class="mr-3 -ml-1 size-24 animate-spin text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" > <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle> <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" > </path> </svg>
examples/sveltekit-sample/src/lib/store.ts +5 −8 Original line number Diff line number Diff line Loading @@ -22,14 +22,11 @@ class PostStore { return this.#map.get(id.toString()); } get = this.#get.bind(this); async #getAll() { return await Array.fromAsync( this.#timeline.reverse() #getAll() { return this.#timeline.reverse() .map((id) => id.toString()) .map((id) => this.#map.get(id)!) .filter((p) => p) .map((p) => p.toJsonLd()), ); .filter((p) => p); } getAll = this.#getAll.bind(this); #delete(id: URL) { Loading
examples/sveltekit-sample/src/routes/users/[identifier]/+page.svelte +2 −18 Original line number Diff line number Diff line Loading @@ -2,6 +2,7 @@ import type { PageProps } from "./$types"; import { browser } from "$app/environment"; import type { Person } from "@fedify/fedify"; import Spinner from "$lib/components/Spinner.svelte"; let { params }: PageProps = $props(); const { identifier } = params; Loading @@ -17,24 +18,7 @@ {#await data} <!-- promise is pending --> <div class="flex h-svh w-svw items-center justify-center"> <svg class="mr-3 -ml-1 size-24 animate-spin text-blue-500" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" ><circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" ></circle><path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" ></path></svg > <Spinner /> </div> {:then user} {#if user} Loading
examples/sveltekit-sample/src/routes/users/[identifier]/posts/+page.svelte +67 −13 Original line number Diff line number Diff line <script lang="ts"> import type { PageProps } from "./$types"; import { browser } from "$app/environment"; import { getPosts } from "./data.remote"; import Spinner from "$lib/components/Spinner.svelte"; import type { Person } from "@fedify/fedify"; let { params }: PageProps = $props(); const { identifier } = params; const query = getPosts(); const data = browser ? fetch(`/users/${identifier}`, { headers: { Accept: "application/activity+json" }, }).then( (res) => res.json() as Promise<Person & { icon: { url: string } }>, ) : Promise.resolve(null); </script> <form method="POST" action="?/post"> <form method="POST" action="?/post" class="post-form"> <input name="identifier" type="hidden" value={identifier} /> <label> Content <input name="content" type="text" /> <div class="form-group"> <label class="form-label"> 새 포스트 작성 <textarea name="content" class="form-textarea" placeholder="무엇을 생각하고 계신가요?" rows="3" ></textarea> </label> <button>Post</button> </div> <button type="submit" class="post-button">게시하기</button> </form> {#if query.error} <p>oops!</p> <div class="error-state"> <p>포스트를 불러오는 중 오류가 발생했습니다.</p> </div> {:else if query.loading} <p>loading...</p> <div class="loading-state"> <Spinner /> <p>포스트를 불러오는 중...</p> </div> {:else if query.current} <ul> <div class="posts-container"> <h2 class="posts-title">포스트 목록</h2> <div class="posts-grid"> {#each query.current as note} <pre>{JSON.stringify(note, null, 2)}</pre> <article class="post-card"> <a href={note.url} class="post-link"> <div class="post-header"> {#await data} <div class="skeleton-avatar"></div> <div class="skeleton-info"> <div class="skeleton-line skeleton-name"></div> <div class="skeleton-line skeleton-handle"></div> </div> {:then user} {#if user} <img src={user.icon?.url ?? "/demo-profile.png"} alt="{user.name}'s profile" class="post-avatar" /> <div class="post-user-info"> <h3 class="post-user-name">{user.name}</h3> <p class="post-user-handle"> @{identifier}@{window.location.host} </p> </div> {/if} {/await} </div> <div class="post-content"> <p>{note.content}</p> </div> </a> </article> {/each} </ul> </div> </div> {/if}