diff --git a/src/css/base/global.css b/src/css/base/global.css index 4c09f28..054582a 100644 --- a/src/css/base/global.css +++ b/src/css/base/global.css @@ -24,11 +24,11 @@ attic-app { [full-start] 1fr [margin-start] - minmax(20px, min(5vi, 100px)) + var(--app-margin) [main-start] minmax(260px, calc((900 / 16) * 1rem)) [main-end] - minmax(20px, min(5vi, 100px)) + var(--app-margin) [margin-end] 1fr [full-end]; @@ -89,3 +89,23 @@ attic-app { anchor-name: --pointer; } } + +dialog { + inline-size: min(min(100%, 900px), calc(100vi - (2 * var(--app-margin)))); + + & [command="close"] { + inset-block-start: 20px; + inset-inline-end: 20px; + position: absolute; + z-index: 1; + + &::before { + content: "×" / ""; + } + } + + &::backdrop { + background: rgb(var(--color-brown) / 0.5); + backdrop-filter: blur(5px); + } +} diff --git a/src/css/base/properties.css b/src/css/base/properties.css index e0bd83f..06131e9 100644 --- a/src/css/base/properties.css +++ b/src/css/base/properties.css @@ -16,4 +16,6 @@ --color-light-yellow: 255 190 50; --color-off-white: 255 234 188; --color-red: 222 34 68; + + --app-margin: max(20px, min(5vi, 100px)); } diff --git a/src/css/components/bookmark.css b/src/css/components/bookmark.css index cfaa4e5..9393641 100644 --- a/src/css/components/bookmark.css +++ b/src/css/components/bookmark.css @@ -51,4 +51,13 @@ white-space: nowrap; pointer-events: none; } + + & > form { + display: contents; + z-index: 1; + + & > button { + z-index: 1; + } + } } diff --git a/src/css/components/button.css b/src/css/components/button.css index a1a883d..27a4aac 100644 --- a/src/css/components/button.css +++ b/src/css/components/button.css @@ -1,11 +1,10 @@ button[type] { - /*background: white;*/ - --font-size: var(--font-size-button); border: 15px solid transparent; border-image: url("/images/button.svg") 15 fill stretch; - block-size: calc(var(--font-size) + 30px); + block-size: calc(var(--font-size-button) + 30px); + color: rgb(var(--color-black)); font-family: var(--font-family-2); - font-size: var(--font-size); + font-size: var(--font-size-button); font-weight: 400; inline-size: fit-content; line-height: 1; @@ -18,4 +17,17 @@ button[type] { &:hover { border-image-source: url("/images/button-hover.svg"); } + + &[data-danger] { + &:not(:hover) { + border-image-source: url("/images/button-danger.svg"); + color: rgb(var(--color-red)); + } + } + + :where(.Bookmark) & { + --font-size-button: var(--font-size-3); + border-width: 10px; + block-size: calc(var(--font-size-button) + 20px); + } } diff --git a/src/css/components/form.css b/src/css/components/form.css index 07fe81e..25a92c7 100644 --- a/src/css/components/form.css +++ b/src/css/components/form.css @@ -81,13 +81,4 @@ form { grid-column: 2 / 3; } } - - &[action*="purge"] { - & button { - &:not(:hover) { - border-image-source: url("/images/button-danger.svg"); - color: rgb(var(--color-red)); - } - } - } } diff --git a/src/css/components/input.css b/src/css/components/input.css index dd920e3..5a0facc 100644 --- a/src/css/components/input.css +++ b/src/css/components/input.css @@ -1,8 +1,8 @@ input { - /*background: white;*/ border: 15px solid transparent; border-image: url("/images/input.svg") 15 fill stretch; block-size: calc(var(--font-size-button) + 30px); + color: rgb(var(--color-black)); font-family: var(--font-family-1); font-size: var(--font-size-3); font-weight: 400; diff --git a/src/css/main.css b/src/css/main.css index f4ee326..61790b2 100644 --- a/src/css/main.css +++ b/src/css/main.css @@ -12,6 +12,7 @@ @import "components/bookmark.css" layer(base); @import "utility/hidden.css" layer(base); +@import "utility/flex.css" layer(base); .error { color: rgb(var(--color-red)); @@ -32,6 +33,7 @@ position-anchor: --pointer; position: fixed; user-select: none; + z-index: 999; @supports not (inset: anchor(start)) { display: none; diff --git a/src/css/utility/flex.css b/src/css/utility/flex.css new file mode 100644 index 0000000..cb05b5b --- /dev/null +++ b/src/css/utility/flex.css @@ -0,0 +1,84 @@ +.flex { + display: flex; + gap: 10px; +} + +.flex-wrap { + flex-wrap: wrap; +} + +.ac-around { + align-content: space-around; +} +.ac-between { + align-content: space-between; +} +.ac-evenly { + align-content: space-evenly; +} +.ac-center { + align-content: center; +} +.ac-end { + align-content: end; +} +.ac-start { + align-content: start; +} +.ac-stretch { + align-content: stretch; +} + +.ai-baseline { + align-items: baseline; +} +.ai-center { + align-items: center; +} +.ai-end { + align-items: end; +} +.ai-start { + align-items: start; +} +.ai-stretch { + align-items: stretch; +} + +.jc-around { + justify-content: space-around; +} +.jc-between { + justify-content: space-between; +} +.jc-evenly { + justify-content: space-evenly; +} +.jc-center { + justify-content: center; +} +.jc-end { + justify-content: end; +} +.jc-start { + justify-content: start; +} +.jc-stretch { + justify-content: stretch; +} + +.ji-baseline { + justify-items: baseline; +} +.ji-center { + justify-items: center; +} +.ji-end { + justify-items: end; +} +.ji-start { + justify-items: start; +} +.ji-stretch { + justify-items: stretch; +} diff --git a/src/lib/valibot.ts b/src/lib/valibot.ts index b87dbaa..93f5497 100644 --- a/src/lib/valibot.ts +++ b/src/lib/valibot.ts @@ -56,6 +56,7 @@ const BookmarkSchema = v.object({ title: v.pipe( v.string(), v.trim(), + v.minLength(1), v.maxLength(2560), v.maxGraphemes(256), ), @@ -63,6 +64,7 @@ const BookmarkSchema = v.object({ v.pipe( v.string(), v.trim(), + v.minLength(1), v.maxLength(320), v.maxGraphemes(32), ), diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index b00a65e..b4ceaac 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -176,7 +176,7 @@ {#if form?.action === "purge" && form?.error}

{form.error}

{/if} - + {:else}
diff --git a/src/routes/bookmarks/[did=did]/+page.server.ts b/src/routes/bookmarks/[did=did]/+page.server.ts index d3483d5..ab1e6bf 100644 --- a/src/routes/bookmarks/[did=did]/+page.server.ts +++ b/src/routes/bookmarks/[did=did]/+page.server.ts @@ -5,6 +5,31 @@ import * as TID from "@atcute/tid"; import { type Actions, fail } from "@sveltejs/kit"; export const actions = { + delete: async (event) => { + if (isAuthEvent(event) === false) { + throw new Error(); + } + const { user } = event.locals; + if (user === undefined) { + return; + } + const formData = await event.request.formData(); + const rpc = new Client({ handler: user.session }); + const result = await rpc.post("com.atproto.repo.deleteRecord", { + input: { + repo: user.did, + collection: "social.attic.bookmark.entity", + rkey: String(formData.get("rkey")), + }, + }); + if (result.ok === false) { + return fail(400, { + action: "delete", + error: "Failed to delete bookmark.", + }); + } + return { success: true }; + }, create: async (event) => { if (isAuthEvent(event) === false) { throw new Error(); @@ -16,9 +41,13 @@ export const actions = { const formData = await event.request.formData(); formData.set("createdAt", new Date().toISOString()); const data = Object.fromEntries(formData); - // @ts-ignore normalize url - data.url = URL.parse(formData.get("url"))?.href; try { + // @ts-ignore normalize url + const parsed = URL.parse(formData.get("url")); + // if (parsed === null) { + // throw new Error("Invalid URL"); + // } + data.url = parsed?.href ?? data.url; const record = parseBookmark(data); const rpc = new Client({ handler: user.session }); const result = await rpc.post("com.atproto.repo.putRecord", { @@ -34,11 +63,14 @@ export const actions = { } return { success: true }; } catch (err) { - console.log(err); + let error = "Failed to create bookmark."; + if (err instanceof Error) { + error = err.message; + } return fail(400, { - data, + data: Object.fromEntries(formData), action: "create", - error: "Failed to create bookmark.", + error, }); } }, diff --git a/src/routes/bookmarks/[did=did]/+page.svelte b/src/routes/bookmarks/[did=did]/+page.svelte index 069e7fc..5d62e65 100644 --- a/src/routes/bookmarks/[did=did]/+page.svelte +++ b/src/routes/bookmarks/[did=did]/+page.svelte @@ -5,6 +5,23 @@ const isSelf = $derived(data.user && params.did === data.user.did); + let createDialog: HTMLDialogElement | null = $state(null); + + $effect(() => { + if (form?.action === "create" && "error" in form) { + if (createDialog?.open === false) { + createDialog?.showModal(); + } + } + }); + + // [TODO] better error dialog? + $effect(() => { + if (form?.action === "delete") { + if (form.error) alert(form.error); + } + }); + const dateFormat = new Intl.DateTimeFormat(undefined, { dateStyle: "medium", timeStyle: "short", @@ -22,37 +39,57 @@

{data.profile.displayName}

{#if isSelf} - -

Create bookmark

-

Please remember: all atproto data is public.

- {#if form?.action === "create" && form?.error} -

{form.error}

- {/if} - - - - - -
+ + +
+

Create bookmark

+

Please remember: all atproto data is public.

+ {#if form?.action === "create" && form?.error} +

{form.error}

+ {/if} + + + + + +
+
{/if} {#if data.bookmarks.length}
-

Bookmarks

+
+

Bookmarks

+ {#if isSelf} + + {/if} +
{#each data.bookmarks as entry (entry.cid)}

@@ -64,6 +101,23 @@ {dateFormat.format(new Date(entry.createdAt))} + {#if isSelf} +
+ + +
+ {/if}

{/each}