bun.lockb
bun.lockb
This is a binary file and will not be displayed.
-1
package.json
-1
package.json
+4
src/app.css
+4
src/app.css
+6
-20
src/lib/components/BookmarkCard.svelte
+6
-20
src/lib/components/BookmarkCard.svelte
···
1
1
<script lang="ts">
2
+
import TagPill from "./TagPill.svelte";
2
3
import type { LexiconCommunityBookmark } from "$lib/utils";
3
4
4
5
type BookmarkCardProps = {
5
-
isOwner: boolean;
6
+
isOwner?: boolean;
6
7
bookmark: LexiconCommunityBookmark;
7
8
onTagClick: (tag: string) => void;
8
9
onTagDeleteClick?: (tag: string) => void;
9
10
};
10
11
11
-
let { isOwner, bookmark, onTagClick, onTagDeleteClick }: BookmarkCardProps = $props();
12
+
let { isOwner = false, bookmark, onTagClick, onTagDeleteClick }: BookmarkCardProps = $props();
12
13
</script>
13
14
14
15
<article class="flex flex-col gap-4 border border-dashed hover:border-solid hover:shadow-lg px-4 py-3 w-fit">
15
-
<a href={bookmark.subject} class="hover:cursor-pointer text-xl visited:text-violet-600">{bookmark.subject}</a>
16
+
<a href={bookmark.subject} class="hover:underline hover:cursor-pointer hover:text-shadow-md text-xl visited:text-violet-600">{bookmark.subject}</a>
16
17
{#if bookmark.tags && bookmark.tags.length > 0}
17
-
<div class="flex gap-5">
18
+
<div class="flex gap-5 flex-wrap">
18
19
{#each bookmark.tags as tag}
19
-
<div class="relative group">
20
-
{#if isOwner}
21
-
<button
22
-
onclick={() => onTagDeleteClick?.(tag)}
23
-
class="absolute -right-3 -top-3 group-hover:block hover:cursor-pointer hidden border bg-red-500 text-white text-xs px-1"
24
-
>
25
-
🗑️
26
-
</button>
27
-
{/if}
28
-
<button
29
-
onclick={() => onTagClick(tag)}
30
-
class="bg-gray-200 w-fit px-2 py-1 hover:cursor-pointer font-comico text-sm"
31
-
>
32
-
{tag}
33
-
</button>
34
-
</div>
20
+
<TagPill {tag} showDeleteButton={isOwner} {onTagClick} {onTagDeleteClick} />
35
21
{/each}
36
22
</div>
37
23
{:else}
+39
src/lib/components/TagPill.svelte
+39
src/lib/components/TagPill.svelte
···
1
+
<script lang="ts">
2
+
type TagPillProps = {
3
+
tag: string;
4
+
variant?: "menu";
5
+
showDeleteButton?: boolean;
6
+
onTagClick?: (tag: string) => void;
7
+
onTagDeleteClick?: (tag: string) => void;
8
+
}
9
+
10
+
let { tag, variant, showDeleteButton, onTagClick, onTagDeleteClick }: TagPillProps = $props();
11
+
</script>
12
+
13
+
<div class="relative group flex">
14
+
{#if showDeleteButton && variant !== "menu"}
15
+
<button
16
+
onclick={() => onTagDeleteClick?.(tag)}
17
+
class="absolute -right-3 -top-3 lg:group-hover:block hover:cursor-pointer hidden bg-white hover:bg-red-500/20 text-white text-xs px-1 py-0.5"
18
+
>
19
+
❌
20
+
</button>
21
+
{/if}
22
+
<button
23
+
onclick={() => onTagClick?.(tag)}
24
+
class={[
25
+
variant === "menu" && "hover:bg-red-300",
26
+
"bg-gray-200 w-fit px-2 py-1 hover:cursor-pointer font-comico text-sm"
27
+
]}
28
+
>
29
+
{tag}
30
+
</button>
31
+
{#if showDeleteButton}
32
+
<button
33
+
onclick={() => onTagDeleteClick?.(tag)}
34
+
class="lg:hidden text-xs px-1.5 py-0.5 border-2 border-gray-200"
35
+
>
36
+
❌
37
+
</button>
38
+
{/if}
39
+
</div>
-1
src/lib/server/api.ts
-1
src/lib/server/api.ts
+24
-21
src/routes/+layout.svelte
+24
-21
src/routes/+layout.svelte
···
1
1
<script lang="ts">
2
2
import { page } from '$app/state';
3
-
import '../app.css';
3
+
import '../app.css';
4
4
5
5
let { data, children } = $props();
6
6
const user = $derived(data.user);
7
7
</script>
8
8
9
9
<div class="flex flex-col gap-8 w-screen h-full min-h-screen font-neco">
10
-
<header class="flex items-center w-full gap-4 px-8 py-4 justify-between">
11
-
<nav class="text-lg flex gap-4 items-center">
12
-
<a href="/" class="font-comico text-2xl hover:text-shadow-md">potatonet.app</a>
13
-
<a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg">🧶</a>
14
-
<a href="https://bsky.app/profile/zeu.dev" class="hover:text-shadow-lg">🦋</a>
15
-
</nav>
10
+
<header class="flex flex-col lg:flex-row lg:items-center w-full gap-4 px-8 py-4 border-b lg:border-none justify-between">
11
+
<a href="/" class="font-comico text-2xl hover:text-shadow-md">potatonet.app</a>
16
12
17
-
<div class="flex gap-4 items-center text-lg">
13
+
<div class="flex gap-4 items-center text-lg flex-wrap">
14
+
<nav class="text-lg flex gap-4 flex-wrap items-center border-3 border-groove px-3 py-1.5">
15
+
<a href="/" class="hover:text-shadow-lg hover:underline" title="explore" aria-label="explore">🛰️ explore</a>
16
+
<a href="https://tangled.sh/@zeu.dev/potatonet-app" class="hover:text-shadow-lg" title="source code" aria-label="source code">🧶 source code</a>
17
+
<a href="https://bsky.app/profile/zeu.dev" class="hover:text-shadow-lg" title="maker's bluesky" aria-label="maker's bluesky">🦋 maker's bluesky</a>
18
+
{#if user}
19
+
<a href={`/${user.handle}/bookmarks`} class="hover:text-shadow-lg" aria-label="logged in user's bookmarks">🔖 your bookmarks</a>
20
+
{/if}
21
+
</nav>
18
22
{#if user}
19
-
<a href={`/${user.handle}/home`} class="hover:text-shadow-lg">🏡</a>
20
23
<form action="/?/logout" method="POST">
21
24
<button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico">
22
25
Logout
23
26
</button>
24
27
</form>
25
28
{:else}
26
-
<form action="/?/login" method="POST" class="flex gap-4">
29
+
<form action="/?/login" method="POST" class="flex gap-4 lg:basis-0">
27
30
<input
28
31
name="handle"
29
32
type="text"
30
33
placeholder="Handle (eg: zeu.dev)"
31
-
class="border border-black border-dashed px-3 py-2 hover:shadow-lg focus:shadow-lg"
34
+
class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg"
32
35
/>
33
-
<button type="submit" class="hover:text-shadow-lg hover:cursor-pointer font-comico">
36
+
<button type="submit" class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">
34
37
Login
35
38
</button>
36
39
</form>
···
39
42
</header>
40
43
41
44
{#key page.url.pathname}
42
-
<main class="flex flex-col gap-4 p-8">
43
-
<svelte:boundary>
44
-
{@render children()}
45
-
46
-
{#snippet pending()}
47
-
<p>Page loading...</p>
48
-
{/snippet}
49
-
</svelte:boundary>
50
-
</main>
45
+
<main class="flex flex-col gap-4 p-8 pt-0 lg:pt-8">
46
+
<svelte:boundary>
47
+
{@render children()}
48
+
49
+
{#snippet pending()}
50
+
<p>Loading...</p>
51
+
{/snippet}
52
+
</svelte:boundary>
53
+
</main>
51
54
{/key}
52
55
</div>
53
56
+40
-26
src/routes/+page.svelte
+40
-26
src/routes/+page.svelte
···
1
1
<script lang="ts">
2
2
import BookmarkCard from "$lib/components/BookmarkCard.svelte";
3
+
import TagPill from "$lib/components/TagPill.svelte";
3
4
import { getAllBookmarks } from "./api/bookmarks/data.remote";
4
5
5
6
let { data } = $props();
6
7
let cursor = $state("");
7
8
const userBookmarksQuery = $derived(getAllBookmarks({ cursor }));
9
+
const queryData = $derived(userBookmarksQuery.current);
8
10
9
11
let query = $state("");
10
12
let filterTags = $state<string[]>([]);
···
21
23
}
22
24
</script>
23
25
24
-
<h1 class="text-3xl font-bold font-comico">explore</h1>
26
+
<div class="flex gap-4 items-center">
27
+
<h1 class="text-2xl lg:text-3xl font-comico">Explore</h1>
28
+
<h2 class="text-lg italic">recent 50</h2>
29
+
</div>
30
+
31
+
<menu class="flex flex-col lg:flex-row w-full gap-4">
32
+
<label class="flex items-center gap-2">
33
+
Search URLs:
34
+
<input type="text" bind:value={query} class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" placeholder="recipe" />
35
+
</label>
36
+
37
+
<label class="flex items-center gap-2">
38
+
Tags:
39
+
{#if filterTags.length === 0}
40
+
<TagPill tag="all" />
41
+
{:else}
42
+
{#each filterTags as filtered}
43
+
<TagPill showDeleteButton tag={filtered} {onTagClick} onTagDeleteClick={onTagClick} variant="menu" />
44
+
{/each}
45
+
{/if}
46
+
</label>
47
+
<button
48
+
onclick={() => userBookmarksQuery.refresh()}
49
+
class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"
50
+
>
51
+
Refresh
52
+
</button>
53
+
{#if data.user}
54
+
<button class="justify-self-end font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">
55
+
🔖 New Bookmark
56
+
</button>
57
+
{/if}
58
+
59
+
</menu>
60
+
<hr />
25
61
26
62
{#if userBookmarksQuery.loading}
27
63
<p>Loading...</p>
28
64
{:else if userBookmarksQuery.error}
29
65
<p>Error</p>
30
-
{:else}
31
-
{@const { cursor: returnedCursor, bookmarks } = userBookmarksQuery.current || { cursor: "", bookmarks: []}}
32
-
33
-
<div class="sticky top-0 flex flex-col gap-4 pt-4 bg-white z-50">
34
-
<menu class="flex justify-between font-comico">
35
-
<div class="flex gap-4">
36
-
<label class="flex items-center gap-2">
37
-
Search term:
38
-
<input type="text" bind:value={query} class="font-neco border px-2 py-1" placeholder="recipe" />
39
-
</label>
40
-
41
-
<label class="flex items-center gap-2">
42
-
Tags:
43
-
{#each filterTags as filtered}
44
-
<button onclick={() => onTagClick(filtered)}>{filtered}</button>
45
-
{/each}
46
-
</label>
47
-
<button onclick={() => userBookmarksQuery.refresh()}>Refresh</button>
48
-
</div>
49
-
</menu>
50
-
<hr />
51
-
</div>
52
-
66
+
{:else if queryData}
53
67
<div class="flex flex-wrap gap-4">
54
-
{#each bookmarks as bookmark}
68
+
{#each queryData.bookmarks as bookmark}
55
69
{#if bookmark.subject.includes(query) && (bookmark.tags?.some(t => filterTags.length > 0 ? filterTags.includes(t) : true))}
56
-
<BookmarkCard isOwner={false} {bookmark} {onTagClick} {onTagDeleteClick} />
70
+
<BookmarkCard {bookmark} {onTagClick} {onTagDeleteClick} />
57
71
{/if}
58
72
{/each}
59
73
</div>
+75
src/routes/[handle]/bookmarks/+page.svelte
+75
src/routes/[handle]/bookmarks/+page.svelte
···
1
+
<script lang="ts">
2
+
import { page } from "$app/state";
3
+
import BookmarkCard from "$lib/components/BookmarkCard.svelte";
4
+
import TagPill from "$lib/components/TagPill.svelte";
5
+
import { getUserBookmarks } from "../../api/bookmarks/data.remote";
6
+
7
+
let { data } = $props();
8
+
const { handle } = page.params;
9
+
let isOwner = $derived(data.user?.handle === handle);
10
+
let cursor = $state("");
11
+
const userBookmarksQuery = $derived(getUserBookmarks({ handle: handle as string, cursor }));
12
+
13
+
let query = $state("");
14
+
let filterTags = $state<string[]>([]);
15
+
16
+
function onTagClick(tag: string) {
17
+
const index = filterTags.findIndex((t) => t === tag);
18
+
if (index >= 0) { filterTags.splice(index, 1); }
19
+
else {
20
+
filterTags.push(tag);
21
+
}
22
+
}
23
+
24
+
function onTagDeleteClick(tag: string) {
25
+
console.log("DELETE", tag);
26
+
}
27
+
</script>
28
+
29
+
<h1 class="text-2xl lg:text-3xl font-comico">Bookmarks by @{handle}</h1>
30
+
31
+
<menu class="flex flex-col lg:flex-row w-full gap-4">
32
+
<label class="flex items-center gap-2">
33
+
Search URLs:
34
+
<input type="text" bind:value={query} class="border border-black border-dashed text-sm px-3 py-2 hover:shadow-lg focus:shadow-lg" placeholder="recipe" />
35
+
</label>
36
+
37
+
<label class="flex items-center gap-2">
38
+
Tags:
39
+
{#if filterTags.length === 0}
40
+
<TagPill tag="all" />
41
+
{:else}
42
+
{#each filterTags as filtered}
43
+
<TagPill showDeleteButton tag={filtered} {onTagClick} onTagDeleteClick={onTagClick} variant="menu" />
44
+
{/each}
45
+
{/if}
46
+
</label>
47
+
<button
48
+
onclick={() => userBookmarksQuery.refresh()}
49
+
class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2"
50
+
>
51
+
Refresh
52
+
</button>
53
+
{#if isOwner}
54
+
<button class="justify-self-end font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">
55
+
🔖 New Bookmark
56
+
</button>
57
+
{/if}
58
+
59
+
</menu>
60
+
<hr />
61
+
62
+
{#if userBookmarksQuery.loading}
63
+
<p>Loading...</p>
64
+
{:else if userBookmarksQuery.error}
65
+
<p>Error</p>
66
+
{:else}
67
+
{@const { cursor: returnedCursor, bookmarks } = userBookmarksQuery.current || { cursor: "", bookmarks: []}}
68
+
<div class="flex flex-wrap gap-4">
69
+
{#each bookmarks as bookmark}
70
+
{#if bookmark.subject.includes(query) && (bookmark.tags?.every(t => filterTags.length > 0 ? filterTags.includes(t) : true))}
71
+
<BookmarkCard {isOwner} {bookmark} {onTagClick} {onTagDeleteClick} />
72
+
{/if}
73
+
{/each}
74
+
</div>
75
+
{/if}
-67
src/routes/[handle]/home/+page.svelte
-67
src/routes/[handle]/home/+page.svelte
···
1
-
<script lang="ts">
2
-
import { page } from "$app/state";
3
-
import BookmarkCard from "$lib/components/BookmarkCard.svelte";
4
-
import { getUserBookmarks } from "../../api/bookmarks/data.remote";
5
-
6
-
let { data } = $props();
7
-
const { handle } = page.params;
8
-
let isOwner = $derived(data.user?.handle === handle);
9
-
let cursor = $state("");
10
-
const userBookmarksQuery = $derived(getUserBookmarks({ handle: handle as string, cursor }));
11
-
12
-
let query = $state("");
13
-
let filterTags = $state<string[]>([]);
14
-
15
-
function onTagClick(tag: string) {
16
-
const index = filterTags.findIndex((t) => t === tag);
17
-
if (index >= 0) { filterTags.splice(index, 1); }
18
-
else {
19
-
filterTags.push(tag);
20
-
}
21
-
}
22
-
23
-
function onTagDeleteClick(tag: string) {
24
-
console.log("DELETE", tag);
25
-
}
26
-
</script>
27
-
28
-
<h1 class="text-3xl font-comico">Bookmarks by @{handle}</h1>
29
-
30
-
{#if userBookmarksQuery.loading}
31
-
<p>Loading...</p>
32
-
{:else if userBookmarksQuery.error}
33
-
<p>Error</p>
34
-
{:else}
35
-
{@const { cursor: returnedCursor, bookmarks } = userBookmarksQuery.current || { cursor: "", bookmarks: []}}
36
-
<menu class="flex justify-between">
37
-
<div class="flex gap-4">
38
-
<label class="flex items-center gap-2">
39
-
Search term:
40
-
<input type="text" bind:value={query} class="border px-2 py-1" placeholder="recipe" />
41
-
</label>
42
-
43
-
<label class="flex items-center gap-2">
44
-
Tags:
45
-
{#each filterTags as filtered}
46
-
<button onclick={() => onTagClick(filtered)}>{filtered}</button>
47
-
{/each}
48
-
</label>
49
-
<button onclick={() => userBookmarksQuery.refresh()}>Refresh</button>
50
-
</div>
51
-
52
-
{#if isOwner}
53
-
<button class="font-comico bg-amber-400 text-black hover:cursor-pointer hover:bg-amber-500 hover:text-white px-4 py-2">
54
-
🔖 New Bookmark
55
-
</button>
56
-
{/if}
57
-
</menu>
58
-
<hr />
59
-
60
-
<div class="flex flex-wrap gap-4">
61
-
{#each bookmarks as bookmark}
62
-
{#if bookmark.subject.includes(query) && (bookmark.tags?.some(t => filterTags.length > 0 ? filterTags.includes(t) : true))}
63
-
<BookmarkCard {isOwner} {bookmark} {onTagClick} {onTagDeleteClick} />
64
-
{/if}
65
-
{/each}
66
-
</div>
67
-
{/if}