Browse Source

Menus

master
Nuša Pukšič 7 months ago committed by buttercat1791
parent
commit
06fb80f73b
  1. 10
      deno.lock
  2. 13
      package-lock.json
  3. 1
      package.json
  4. 147
      src/app.css
  5. 1
      src/lib/a/index.ts
  6. 2
      src/lib/a/nav/AFooter.svelte
  7. 87
      src/lib/a/nav/ANavbar.svelte
  8. 41
      src/lib/a/nav/AUserDropdown.svelte
  9. 15
      src/lib/a/nav/nav-types.ts
  10. 2
      src/lib/a/primitives/APagination.svelte
  11. 10
      src/lib/a/primitives/ASwitch.svelte
  12. 4
      src/lib/a/reader/ATechToggle.svelte
  13. 268
      src/lib/components/util/Profile.svelte
  14. 41
      src/lib/nav/site-nav.ts
  15. 2
      src/routes/+layout.svelte
  16. 3
      src/routes/profile/+page.svelte
  17. 10
      src/routes/profile/my-notes/+page.svelte

10
deno.lock

@ -32,6 +32,7 @@
"npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.38.2__acorn@8.15.0_tailwind-merge@3.3.1", "npm:flowbite-svelte-icons@2.1": "2.1.1_svelte@5.38.2__acorn@8.15.0_tailwind-merge@3.3.1",
"npm:flowbite-svelte@0.48": "0.48.6_svelte@5.38.2__acorn@8.15.0", "npm:flowbite-svelte@0.48": "0.48.6_svelte@5.38.2__acorn@8.15.0",
"npm:flowbite-svelte@1.11": "1.11.8_svelte@5.38.2__acorn@8.15.0_tailwindcss@4.1.12_tailwind-merge@3.3.1", "npm:flowbite-svelte@1.11": "1.11.8_svelte@5.38.2__acorn@8.15.0_tailwindcss@4.1.12_tailwind-merge@3.3.1",
"npm:flowbite-typography@^1.0.5": "1.0.5",
"npm:flowbite@2": "2.5.2", "npm:flowbite@2": "2.5.2",
"npm:flowbite@~2.5.2": "2.5.2", "npm:flowbite@~2.5.2": "2.5.2",
"npm:he@1.2": "1.2.0", "npm:he@1.2": "1.2.0",
@ -2104,6 +2105,14 @@
"tailwindcss" "tailwindcss"
] ]
}, },
"flowbite-typography@1.0.5": {
"integrity": "sha512-IqTwOYgGZkXTK/5ngx3A9oQwgOqnRyUKUfIiB+w6xDmiD8z3cKDIgYfFpHIMKbLVfg+QmJIPqEEPrGZbAwVT6g==",
"dependencies": [
"lodash.castarray",
"lodash.isplainobject",
"lodash.merge"
]
},
"flowbite@2.5.2": { "flowbite@2.5.2": {
"integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==", "integrity": "sha512-kwFD3n8/YW4EG8GlY3Od9IoKND97kitO+/ejISHSqpn3vw2i5K/+ZI8Jm2V+KC4fGdnfi0XZ+TzYqQb4Q1LshA==",
"dependencies": [ "dependencies": [
@ -3581,6 +3590,7 @@
"npm:eslint-plugin-svelte@^3.11.0", "npm:eslint-plugin-svelte@^3.11.0",
"npm:flowbite-svelte-icons@2.1", "npm:flowbite-svelte-icons@2.1",
"npm:flowbite-svelte@1.11", "npm:flowbite-svelte@1.11",
"npm:flowbite-typography@^1.0.5",
"npm:flowbite@~2.5.2", "npm:flowbite@~2.5.2",
"npm:he@1.2", "npm:he@1.2",
"npm:highlight.js@^11.11.1", "npm:highlight.js@^11.11.1",

13
package-lock.json generated

@ -45,6 +45,7 @@
"flowbite": "~2.5.2", "flowbite": "~2.5.2",
"flowbite-svelte": "1.11.x", "flowbite-svelte": "1.11.x",
"flowbite-svelte-icons": "2.1.x", "flowbite-svelte-icons": "2.1.x",
"flowbite-typography": "^1.0.5",
"playwright": "^1.50.1", "playwright": "^1.50.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-load-config": "6.x", "postcss-load-config": "6.x",
@ -4325,6 +4326,18 @@
"postcss": "^8.5.1" "postcss": "^8.5.1"
} }
}, },
"node_modules/flowbite-typography": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/flowbite-typography/-/flowbite-typography-1.0.5.tgz",
"integrity": "sha512-IqTwOYgGZkXTK/5ngx3A9oQwgOqnRyUKUfIiB+w6xDmiD8z3cKDIgYfFpHIMKbLVfg+QmJIPqEEPrGZbAwVT6g==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2"
}
},
"node_modules/fraction.js": { "node_modules/fraction.js": {
"version": "4.3.7", "version": "4.3.7",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz",

1
package.json

@ -54,6 +54,7 @@
"flowbite": "~2.5.2", "flowbite": "~2.5.2",
"flowbite-svelte": "1.11.x", "flowbite-svelte": "1.11.x",
"flowbite-svelte-icons": "2.1.x", "flowbite-svelte-icons": "2.1.x",
"flowbite-typography": "^1.0.5",
"playwright": "^1.50.1", "playwright": "^1.50.1",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"postcss-load-config": "6.x", "postcss-load-config": "6.x",

147
src/app.css

@ -14,6 +14,8 @@
@layer theme, base, components, utilities; @layer theme, base, components, utilities;
@plugin "flowbite/plugin"; @plugin "flowbite/plugin";
@plugin "flowbite-typography";
@custom-variant dark (&:where(.dark, .dark *)); @custom-variant dark (&:where(.dark, .dark *));
@theme { @theme {
@ -455,6 +457,11 @@
} }
@layer components { @layer components {
nav a {
text-decoration-line: none !important;
}
canvas.qr-code { canvas.qr-code {
@apply block mx-auto my-4; @apply block mx-auto my-4;
} }
@ -462,11 +469,11 @@
/* Legend */ /* Legend */
.leather-legend { .leather-legend {
@apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2 @apply relative m-4 sm:m-0 sm:absolute sm:top-1 sm:left-1 flex-shrink-0 p-2
rounded; rounded;
@apply shadow-none text-primary-1000 border border-s-4 bg-highlight @apply shadow-none text-primary-1000 border border-s-4 bg-highlight
border-primary-200 has-[:hover]:border-primary-700; border-primary-200 has-[:hover]:border-primary-700;
@apply dark:bg-primary-1000 dark:border-primary-800 @apply dark:bg-primary-1000 dark:border-primary-800
dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500; dark:has-[:hover]:bg-primary-950 dark:has-[:hover]:border-primary-500;
max-width: 450px; max-width: 450px;
min-width: 300px; min-width: 300px;
overflow-x: auto; overflow-x: auto;
@ -484,90 +491,106 @@
@apply dark:text-white; @apply dark:text-white;
} }
.publication-leather { .publication-leather {
@apply flex flex-col space-y-4; @apply flex flex-col space-y-4;
scroll-margin-top: 150px; scroll-margin-top: 150px;
scroll-behavior: smooth; scroll-behavior: smooth;
/* common heading base styles */ /* common heading base styles */
h1,h2,h3,h4,h5,h6 { @apply text-gray-900 dark:text-gray-100 pt-4; }
h1, h2, h3, h4, h5, h6 {
@apply text-gray-900 dark:text-gray-100 pt-4;
}
/* sizes/weights per level */ /* sizes/weights per level */
h1 { @apply text-4xl font-bold; }
h2 { @apply text-3xl font-bold; } h1 {
h3 { @apply text-2xl font-bold; } @apply text-4xl font-bold;
h4 { @apply text-xl font-bold; }
h5 { @apply text-lg font-semibold; }
h6 { @apply text-base font-semibold; }
} }
div { h2 {
@apply flex flex-col space-y-4; @apply text-3xl font-bold;
} }
.olist { h3 {
@apply flex flex-col space-y-4; @apply text-2xl font-bold;
}
ol { h4 {
@apply list-decimal px-6 flex flex-col space-y-2; @apply text-xl font-bold;
}
li { h5 {
.paragraph { @apply text-lg font-semibold;
@apply py-2;
}
}
}
} }
.ulist { h6 {
@apply flex flex-col space-y-4; @apply text-base font-semibold;
}
}
.olist {
@apply flex flex-col space-y-4;
ul { ol {
@apply list-disc px-6 flex flex-col space-y-2; @apply list-decimal px-6 flex flex-col space-y-2;
li { li {
.paragraph { .paragraph {
@apply py-2; @apply py-2;
}
} }
} }
} }
}
a { .ulist {
@apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400; @apply flex flex-col space-y-4;
}
.imageblock { ul {
@apply flex flex-col items-center; @apply list-disc px-6 flex flex-col space-y-2;
.title { li {
@apply text-sm text-center; .paragraph {
@apply py-2;
}
} }
} }
}
.stemblock { a {
@apply bg-gray-200 dark:bg-gray-800 p-4 rounded-lg; @apply underline cursor-pointer hover:text-primary-600 dark:hover:text-primary-400;
}
.imageblock {
@apply flex flex-col items-center;
.title {
@apply text-sm text-center;
} }
}
.literalblock { .stemblock {
pre { @apply bg-gray-200 dark:bg-gray-800 p-4 rounded-lg;
@apply text-wrap; }
}
.literalblock {
pre {
@apply text-wrap;
} }
}
table { table {
@apply w-full overflow-x-auto; @apply w-full overflow-x-auto;
caption { caption {
@apply text-sm; @apply text-sm;
} }
thead, thead,
tbody { tbody {
th, th,
td { td {
@apply border border-gray-200 dark:border-gray-700; @apply border border-gray-200 dark:border-gray-700;
}
} }
} }
} }
@ -597,6 +620,7 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
.footnotes li { .footnotes li {
margin-bottom: 0.5rem; margin-bottom: 0.5rem;
} }
@ -684,7 +708,7 @@
/* Table of Contents highlighting */ /* Table of Contents highlighting */
.toc-highlight { .toc-highlight {
@apply bg-primary-200 dark:bg-primary-700 border-l-4 border-primary-600 @apply bg-primary-200 dark:bg-primary-700 border-l-4 border-primary-600
dark:border-primary-400 font-medium; dark:border-primary-400 font-medium;
transition: all 0.2s ease-in-out; transition: all 0.2s ease-in-out;
} }
@ -720,9 +744,10 @@
} }
} }
.icon-wiki { .icon-wiki {
font-size: 20px; font-size: 20px;
line-height: 20px; line-height: 20px;
vertical-align: text-bottom; vertical-align: text-bottom;
font-weight: 500; font-weight: 500;
}
} }

1
src/lib/a/index.ts

@ -1,6 +1,5 @@
export { default as AInput } from './primitives/AInput.svelte'; export { default as AInput } from './primitives/AInput.svelte';
export { default as ACard } from './primitives/ACard.svelte'; export { default as ACard } from './primitives/ACard.svelte';
export { default as ASwitch } from './primitives/ASwitch.svelte';
export { default as ADetails } from './primitives/ADetails.svelte'; export { default as ADetails } from './primitives/ADetails.svelte';
export { default as ANostrUser } from './primitives/ANostrUser.svelte'; export { default as ANostrUser } from './primitives/ANostrUser.svelte';
export { default as ANostrBadge } from './primitives/ANostrBadge.svelte'; export { default as ANostrBadge } from './primitives/ANostrBadge.svelte';

2
src/lib/a/nav/AFooter.svelte

@ -2,7 +2,7 @@
import { Footer, FooterCopyright, FooterLink, FooterLinkGroup } from "flowbite-svelte"; import { Footer, FooterCopyright, FooterLink, FooterLinkGroup } from "flowbite-svelte";
</script> </script>
<Footer class="mx-2"> <Footer class="m-2">
<FooterCopyright href="/events?id=npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" by="GitCitadel" year={2025} /> <FooterCopyright href="/events?id=npub1s3ht77dq4zqnya8vjun5jp3p44pr794ru36d0ltxu65chljw8xjqd975wz" by="GitCitadel" year={2025} />
<FooterLinkGroup class="mt-3 flex flex-wrap items-center text-sm text-gray-500 sm:mt-0 dark:text-gray-400"> <FooterLinkGroup class="mt-3 flex flex-wrap items-center text-sm text-gray-500 sm:mt-0 dark:text-gray-400">
<FooterLink href="/about">About</FooterLink> <FooterLink href="/about">About</FooterLink>

87
src/lib/a/nav/ANavbar.svelte

@ -6,79 +6,52 @@
NavUl, NavUl,
NavHamburger, NavHamburger,
NavBrand, NavBrand,
Dropdown, MegaMenu
DropdownItem
} from "flowbite-svelte"; } from "flowbite-svelte";
import { siteNav } from "$lib/nav/site-nav.js";
import { logoutUser, userStore } from "$lib/stores/userStore";
import Profile from "$components/util/Profile.svelte"; import Profile from "$components/util/Profile.svelte";
import type { NavItem } from "$lib/a/nav/nav-types.ts";
import { goto } from "$app/navigation";
import { ChevronDownOutline } from "flowbite-svelte-icons"; import { ChevronDownOutline } from "flowbite-svelte-icons";
import { AThemeToggleMini } from "$lib/a"; import { AThemeToggleMini } from "$lib/a";
import { getNdkContext } from "$lib/ndk.ts";
const ndk = getNdkContext();
let { let menu2 = [
currentPath = "", { name: 'Publications', href: '/', help: 'Browse publications' },
}: { { name: 'Events', href: '/events', help: 'Search and engage with events' },
currentPath?: string; { name: 'Visualize', href: '/visualize', help: 'Visualize connections between publications and authors' },
} = $props();
let userState = $derived($userStore); { name: 'Compose notes', href: '/new/compose', help: 'Create individual notes (30041 events)'},
{ name: 'Publish events', href: '/events/compose', help: 'Create any kind' },
function handleNavClick(item: NavItem) { { name: 'Getting Started', href: '/start', help: 'A quick overview and tutorial' },
if (item.href) { { name: 'Relay Status', href: '/about/relay-stats', help: 'Relay status and monitoring' },
goto(item.href); { name: 'About', href: '/about', help: 'About the project' },
} else if (item.id === 'logout') { { name: 'Contact', href: '/contact', help: 'Contact us or submit a bug report' }
logoutUser(ndk); ];
}
}
function flattenNavItems(navItems: NavItem[]): NavItem[] {
const result: NavItem[] = [];
for (const item of navItems) {
if (item.children && item.children.length > 0) {
result.push(...flattenNavItems(item.children));
} else {
result.push(item);
}
}
return result;
}
</script> </script>
<Navbar class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-800 !p-0" navContainerClass="w-full flex-row justify-between items-center !p-0"> <Navbar id="navi" class="fixed start-0 top-0 z-50 flex flex-row bg-primary-50 dark:bg-primary-800 !p-0" navContainerClass="flex-row items-center !p-0">
<NavBrand href="/"> <NavBrand href="/">
<h1>Alexandria</h1> <div class="flex flex-col">
<h1 class="text-2xl font-bold mb-0">Alexandria</h1>
<p class="text-xs font-semibold tracking-wide max-sm:max-w-[11rem] mb-0">READ THE ORIGINAL. MAKE CONNECTIONS. CULTIVATE KNOWLEDGE.</p>
</div>
</NavBrand> </NavBrand>
<div class="flex md:order-2"> <div class="flex md:order-2">
<Profile isNav={true} /> <Profile />
<NavHamburger /> <NavHamburger />
</div> </div>
<NavUl class="order-1" activeUrl={currentPath}> <NavUl class="order-1 ml-auto">
{#each siteNav as navSection} <NavLi class="cursor-pointer">
{#if navSection.children && navSection.children.length > 0} Explore<ChevronDownOutline class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white" />
<NavLi class="cursor-pointer">
{navSection.title}<ChevronDownOutline class="text-primary-800 ms-2 inline h-6 w-6 dark:text-white" />
</NavLi>
<Dropdown simple class="w-44 z-20">
{#each flattenNavItems(navSection.children) as item}
<DropdownItem
href={item.href || undefined}
onclick={() => handleNavClick(item)}
>
{item.title}
</DropdownItem>
{/each}
</Dropdown>
{:else if navSection.href}
<NavLi href={navSection.href}>{navSection.title}</NavLi>
{/if}
{/each}
<NavLi>
</NavLi> </NavLi>
<MegaMenu full items={menu2}>
{#snippet children({ item })}
<a href={item.href} class="block h-full rounded-lg p-3 hover:bg-gray-50 dark:hover:bg-gray-700">
<div class="font-semibold dark:text-white">{item.name}</div>
<span class="text-sm font-light text-gray-500 dark:text-gray-400">{item.help}</span>
</a>
{/snippet}
</MegaMenu>
<AThemeToggleMini /> <AThemeToggleMini />
</NavUl> </NavUl>
</Navbar> </Navbar>

41
src/lib/a/nav/AUserDropdown.svelte

@ -1,41 +0,0 @@
<script lang="ts">
import type { NavItem, UserInfo } from './nav-types';
import { onMount } from 'svelte';
let { user = null as UserInfo | null, items = [] as NavItem[], onselect = undefined as undefined | ((i:NavItem)=>void) } = $props();
let open = $state(false); let btn: HTMLButtonElement;
function onDoc(e: MouseEvent){ if (!open) return; if (!(e.target instanceof Node)) return; if (!btn?.parentElement?.contains(e.target)) open = false; }
onMount(()=>{ document.addEventListener('mousedown', onDoc); return () => document.removeEventListener('mousedown', onDoc); });
</script>
<div class="relative">
<button bind:this={btn} class="inline-flex items-center gap-2 h-9 px-2 rounded-md border border-muted/30 hover:bg-primary/10" aria-haspopup="menu" aria-expanded={open} onclick={() => (open = !open)}>
<img src={user?.avatarUrl || 'https://via.placeholder.com/24'} alt="" class="h-6 w-6 rounded-full object-cover" />
<span class="hidden sm:block text-sm">{user?.name || 'Account'}</span>
<svg class="h-4 w-4 opacity-70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"/></svg>
</button>
{#if open}
<div class="absolute right-0 mt-1 min-w-[14rem] rounded-lg border border-muted/20 bg-surface shadow-lg py-1 z-50">
{#if user}
<div class="px-3 py-2 text-sm">
<div class="font-medium">{user.name}</div>
</div>
<div class="my-1 h-px bg-muted/20" ></div>
{/if}
<ul>
{#each items as it}
{#if it.divider}
<li class="my-1 h-px bg-muted/20"></li>
{:else}
<li>
<a href={it.href || '#'} target={it.external ? '_blank':undefined} rel={it.external ? 'noreferrer':undefined}
class="flex items-center gap-2 px-3 py-2 text-sm hover:bg-primary/10"
onclick={(e)=>{ if (!it.href || it.href==='#'){ e.preventDefault(); open=false; onselect?.(it); } else { open=false; } }}>
{it.title}
{#if it.badge}<span class="ml-auto text-[10px] rounded px-1.5 py-0.5 border border-primary/30 text-primary bg-primary/5">{it.badge}</span>{/if}
</a>
</li>
{/if}
{/each}
</ul>
</div>
{/if}
</div>

15
src/lib/a/nav/nav-types.ts

@ -1,15 +0,0 @@
export type NavItem = {
id?: string;
title: string;
href?: string;
icon?: any;
badge?: string;
children?: NavItem[];
divider?: boolean;
external?: boolean;
};
export type UserInfo = {
name: string;
avatarUrl?: string;
};

2
src/lib/a/primitives/APagination.svelte

@ -37,7 +37,7 @@
{#if totalPages > 1} {#if totalPages > 1}
<div class={`mt-4 flex flex-row items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg ${className}`}> <div class={`mt-4 flex flex-row items-center justify-between p-3 bg-gray-50 dark:bg-gray-800 rounded-lg ${className}`}>
<div class="text-sm text-gray-600 dark:text-gray-400"> <div class="text-sm !mb-0 text-gray-600 dark:text-gray-400">
Page {currentPage} of {totalPages} ({totalItems} total {itemsLabel}) Page {currentPage} of {totalPages} ({totalItems} total {itemsLabel})
</div> </div>
<div class="flex flex-row items-center gap-2"> <div class="flex flex-row items-center gap-2">

10
src/lib/a/primitives/ASwitch.svelte

@ -1,10 +0,0 @@
<script lang="ts">
let { checked = false, class: className = '', onchange = undefined as undefined | ((v:boolean)=>void) } = $props();
function toggle(){ checked = !checked; onchange?.(checked); }
</script>
<button type="button" role="switch" aria-checked={checked}
onclick={toggle}
class={`inline-flex items-center h-6 w-11 rounded-full border border-muted/30 transition px-0.5 ${checked ? 'bg-primary' : 'bg-surface'} ${className}`}
>
<span class={`inline-block h-5 w-5 rounded-full bg-white shadow transform transition ${checked ? 'translate-x-5' : 'translate-x-0'}`} />
</button>

4
src/lib/a/reader/ATechToggle.svelte

@ -1,11 +1,11 @@
<script lang="ts"> <script lang="ts">
import { showTech } from '$lib/stores/techStore.ts'; import { showTech } from '$lib/stores/techStore.ts';
import ASwitch from '../primitives/ASwitch.svelte'; import { Toggle } from "flowbite-svelte";
let label = 'Show technical details'; let label = 'Show technical details';
$: checked = $showTech; $: checked = $showTech;
</script> </script>
<div class="inline-flex items-center gap-2 select-none"> <div class="inline-flex items-center gap-2 select-none">
<ASwitch bind:checked={$showTech} aria-label={label} /> <Toggle {checked} ontoggle={() => $showTech = checked} aria-label={label} />
<span class="text-sm">{label}</span> <span class="text-sm">{label}</span>
</div> </div>

268
src/lib/components/util/Profile.svelte

@ -8,11 +8,8 @@
loginWithAmber, loginWithAmber,
loginWithNpub loginWithNpub
} from "$lib/stores/userStore"; } from "$lib/stores/userStore";
import { import { Avatar, Popover, Dropdown, DropdownGroup, DropdownItem, DropdownHeader } from "flowbite-svelte";
ArrowRightToBracketOutline, import { Globe, Loader, Book, Smartphone } from "@lucide/svelte";
UserOutline,
} from "flowbite-svelte-icons";
import { Avatar, Popover } from "flowbite-svelte";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
@ -22,8 +19,6 @@
const ndk = getNdkContext(); const ndk = getNdkContext();
let { isNav = false } = $props<{ isNav?: boolean }>();
// UI state for login functionality // UI state for login functionality
let isLoadingExtension: boolean = $state(false); let isLoadingExtension: boolean = $state(false);
let isLoadingAmber: boolean = $state(false); let isLoadingAmber: boolean = $state(false);
@ -31,7 +26,6 @@
let nostrConnectUri: string | undefined = $state(undefined); let nostrConnectUri: string | undefined = $state(undefined);
let showQrCode: boolean = $state(false); let showQrCode: boolean = $state(false);
let qrCodeDataUrl: string | undefined = $state(undefined); let qrCodeDataUrl: string | undefined = $state(undefined);
let loginButtonRef: HTMLElement | undefined = $state();
let resultTimeout: ReturnType<typeof setTimeout> | null = null; let resultTimeout: ReturnType<typeof setTimeout> | null = null;
let profileAvatarId = "profile-avatar-btn"; let profileAvatarId = "profile-avatar-btn";
let showAmberFallback = $state(false); let showAmberFallback = $state(false);
@ -390,162 +384,132 @@
<div class="relative h-fit my-auto"> <div class="relative h-fit my-auto">
{#if !userState.signedIn} {#if !userState.signedIn}
<!-- Login button --> <!-- Login button -->
<div class="group"> <Avatar size="xs" id="login-menu"/>
<button <Dropdown placement="bottom" triggeredBy="#login-menu" class="min-w-xs">
bind:this={loginButtonRef} <DropdownGroup>
id="login-avatar" <DropdownHeader>Login with...</DropdownHeader>
class="h-6 w-6 rounded-full bg-gray-300 flex items-center justify-center cursor-pointer hover:bg-gray-400 transition-colors" <DropdownItem
> class="w-full"
<UserOutline class="h-4 w-4 text-gray-600" /> onclick={handleBrowserExtensionLogin}
</button> disabled={isLoadingExtension || isLoadingAmber}>
<Popover <span class="w-full flex items-center justify-start gap-3">
placement="bottom"
triggeredBy="#login-avatar"
class="popover-leather w-[200px]"
trigger="click"
>
<div class="flex flex-col space-y-2">
<h3 class="text-lg font-bold mb-2">Login with...</h3>
<button
class="btn-leather text-nowrap flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500 disabled:opacity-50"
onclick={handleBrowserExtensionLogin}
disabled={isLoadingExtension || isLoadingAmber}
>
{#if isLoadingExtension} {#if isLoadingExtension}
🔄 Connecting... <Loader size={16} class="inline" /> Connecting...
{:else} {:else}
🌐 Browser extension <Globe size={16} class="inline" /> Browser extension
{/if} {/if}
</button> </span></DropdownItem>
<button <DropdownItem
class="btn-leather text-nowrap flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500 disabled:opacity-50" class="w-full"
onclick={handleAmberLogin} onclick={handleAmberLogin}
disabled={isLoadingAmber || isLoadingExtension} disabled={isLoadingAmber || isLoadingExtension}
> >
{#if isLoadingAmber} <span class="w-full flex items-center justify-start gap-3">
🔄 Connecting... {#if isLoadingAmber}
<Loader size={16} class="inline" /> Connecting...
{:else} {:else}
📱 Amber: NostrConnect <Smartphone size={16} class="inline" /> Amber: NostrConnect
{/if} {/if}
</button> </span>
<button </DropdownItem>
class="btn-leather text-nowrap flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500" <DropdownItem
onclick={handleReadOnlyLogin} class="w-full"
> onclick={handleReadOnlyLogin}
📖 npub (read only)
</button>
<div class="border-t border-gray-200 pt-2 mt-2">
<div class="text-xs text-gray-500 mb-1">Network Status:</div>
<NetworkStatus />
</div>
</div>
</Popover>
{#if result}
<div
class="absolute right-0 top-10 z-50 bg-gray-100 p-3 rounded text-sm break-words whitespace-pre-line max-w-lg shadow-lg border border-gray-300"
> >
{result} <span class="w-full flex items-center justify-start gap-3">
<button <Book size={16} class="inline" /> npub (read only)
class="ml-2 text-gray-500 hover:text-gray-700" </span>
onclick={() => (result = null)}>✖</button </DropdownItem>
{#if result}
<DropdownHeader>
<div
class="absolute right-0 top-10 z-50 bg-gray-100 p-3 rounded text-sm break-words whitespace-pre-line max-w-lg shadow-lg border border-gray-300"
> >
</div> {result}
{/if} <button
</div> class="ml-2 text-gray-500 hover:text-gray-700"
onclick={() => (result = null)}>✖</button
>
</div>
</DropdownHeader>
{/if}
</DropdownGroup>
<DropdownGroup>
<DropdownHeader>
<NetworkStatus />
</DropdownHeader>
</DropdownGroup>
</Dropdown>
{:else} {:else}
<!-- User profile --> <!-- User profile -->
<div class="group"> <Avatar
<button src={pfp}
class="h-6 w-6 rounded-full p-0 border-0 bg-transparent cursor-pointer" alt={username || "User"}
id={profileAvatarId} aria-label="Open profile menu"
type="button" size="xs" id={profileAvatarId}/>
aria-label="Open profile menu" <Dropdown placement="bottom" triggeredBy="#{profileAvatarId}" class="min-w-xs">
> <DropdownHeader>
{#if !pfp} {#if username}
<div class="h-6 w-6 rounded-full bg-gray-300 animate-pulse cursor-pointer"></div> <span class="block text-sm">{username}</span>
<span class="block truncate text-sm font-medium">@{tag}</span>
{:else if !pfp}
<span>Loading profile...</span>
{:else} {:else}
<Avatar <span>Loading...</span>
rounded
class="h-6 w-6 cursor-pointer"
src={pfp}
alt={username || "User"}
/>
{/if} {/if}
</button> </DropdownHeader>
<Popover <DropdownGroup>
placement="bottom" <DropdownItem class="w-full">
triggeredBy={`#${profileAvatarId}`} <CopyToClipboard
class="popover-leather w-[220px]" displayText={shortenNpub(npub) || "Loading..."}
trigger="click" copyText={npub || ""}
> />
<div class="flex flex-row justify-between space-x-4"> </DropdownItem>
<div class="flex flex-col"> </DropdownGroup>
{#if username} <DropdownGroup>
<h3 class="text-lg font-bold">{username}</h3> <DropdownItem
{#if isNav}<h4 class="text-base">@{tag}</h4>{/if} class="w-full items-start"
{:else if !pfp} onclick={() => goto('/profile')}
<h3 class="text-lg font-bold">Loading profile...</h3> >
{:else} View profile
<h3 class="text-lg font-bold">Loading...</h3> </DropdownItem>
{/if} <DropdownItem
<ul class="space-y-2 mt-2"> class="w-full"
<li> onclick={() => goto('/profile/my-notes')}
<CopyToClipboard >
displayText={shortenNpub(npub) || "Loading..."} My notes
copyText={npub || ""} </DropdownItem>
/> <DropdownItem
</li> class="w-full"
<li> onclick={() => goto('/profile/notifications')}
<button >
class="hover:text-primary-400 dark:hover:text-primary-500 text-nowrap mt-3 m-0 text-left" Notifications
onclick={handleViewProfile} </DropdownItem>
> </DropdownGroup>
<UserOutline <DropdownGroup>
class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none" <DropdownHeader class="text-xs">
/><span class="underline">View notifications</span> {#if userState.loginMethod === "extension"}
</button> Logged in with extension
</li> {:else if userState.loginMethod === "amber"}
Logged in with Amber
<li class="text-xs text-gray-500"> {:else if userState.loginMethod === "npub"}
{#if userState.loginMethod === "extension"} Logged in with npub
Logged in with extension {:else}
{:else if userState.loginMethod === "amber"} Unknown login method
Logged in with Amber {/if}
{:else if userState.loginMethod === "npub"} </DropdownHeader>
Logged in with npub <DropdownHeader><NetworkStatus /></DropdownHeader>
{:else} </DropdownGroup>
Unknown login method <DropdownGroup>
{/if} <DropdownItem
</li> id="sign-out-button"
<li> class="w-full"
<NetworkStatus /> onclick={handleSignOutClick}
</li> >
{#if isNav} Sign out
<li> </DropdownItem>
<button </DropdownGroup>
id="sign-out-button" </Dropdown>
class="btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500"
onclick={handleSignOutClick}
>
<ArrowRightToBracketOutline
class="mr-1 !h-6 !w-6 inline !fill-none dark:!fill-none"
/> Sign out
</button>
</li>
{:else}
<!-- li>
<button
class='btn-leather text-nowrap mt-3 flex self-stretch align-middle hover:text-primary-400 dark:hover:text-primary-500'
>
<FileSearchOutline class='mr-1 !h-6 inline !fill-none dark:!fill-none' /> More content
</button>
</li -->
{/if}
</ul>
</div>
</div>
</Popover>
</div>
{/if} {/if}
</div> </div>

41
src/lib/nav/site-nav.ts

@ -1,41 +0,0 @@
import type { NavItem } from '$lib/a/nav/nav-types';
export const siteNav: NavItem[] = [
{
title: 'Create',
children: [
{ title: 'Compose notes', href: '/new/compose' },
{ title: 'Publish events', href: '/events/compose' },
]
},
{
title: 'Explore',
children: [
{ title: 'Publications', children: [
{ title: 'Publications', href: '/' },
{ title: 'My Notes', href: '/my-notes' },
{ title: 'Events', href: '/events' },
{ title: 'Visualize', href: '/visualize' }
]
}
]
},
{
title: 'About',
children: [
{ title: 'Onboarding', children: [{ title: 'Getting Started', href: '/start' }] },
{ title: 'Project', children: [
{ title: 'About', href: '/about' },
{ title: 'Contact', href: '/contact' },
{ title: 'Relay Status', href: '/about/relay-stats' }
] }
]
}
];
export const userMenu: NavItem[] = [
{ title: 'Profile', href: '/me' },
{ title: 'Settings', href: '/settings' },
{ divider: true, title: '' },
{ id: 'logout', title: 'Sign out' } // <-- no href => action item
];

2
src/routes/+layout.svelte

@ -183,7 +183,7 @@
</svelte:head> </svelte:head>
<div class="min-h-screen flex flex-col"> <div class="min-h-screen flex flex-col">
<ANavbar {currentPath} /> <ANavbar />
<main class="flex-1 flex-col w-full mt-[75px] self-center"> <main class="flex-1 flex-col w-full mt-[75px] self-center">
{@render children()} {@render children()}

3
src/routes/profile/+page.svelte

@ -128,7 +128,8 @@
{/if} {/if}
</div> </div>
<div class="flex flex-row justify-end gap-4 text-sm"> <div class="flex flex-row justify-end gap-4 text-sm">
<Button size="xs" onclick={() => goto('/profile/notifications')}>Notifications</Button> <Button class="!mb-0" size="xs" onclick={() => goto('/profile/notifications')}>Notifications</Button>
<Button class="!mb-0" size="xs" onclick={() => goto('/profile/my-notes')}>My notes</Button>
</div> </div>
{#if loading} {#if loading}
<AAlert color="primary">Loading profile…</AAlert> <AAlert color="primary">Loading profile…</AAlert>

10
src/routes/my-notes/+page.svelte → src/routes/profile/my-notes/+page.svelte

@ -1,13 +1,13 @@
<script lang="ts"> <script lang="ts">
import { goto } from "$app/navigation"; import { goto } from "$app/navigation";
import { userStore } from "$lib/stores/userStore"; import { userStore } from "$lib/stores/userStore.ts";
import type { NDKEvent } from "@nostr-dev-kit/ndk"; import type { NDKEvent } from "@nostr-dev-kit/ndk";
import { get } from "svelte/store"; import { get } from "svelte/store";
import { getMatchingTags } from "$lib/utils/nostrUtils"; import { getMatchingTags } from "$lib/utils/nostrUtils.ts";
import { getTitleTagForEvent } from "$lib/utils/event_input_utils"; import { getTitleTagForEvent } from "$lib/utils/event_input_utils.ts";
import asciidoctor from "asciidoctor"; import asciidoctor from "asciidoctor";
import { postProcessAsciidoctorHtml } from "$lib/utils/markup/asciidoctorPostProcessor"; import { postProcessAsciidoctorHtml } from "$lib/utils/markup/asciidoctorPostProcessor.ts";
import { getNdkContext } from "$lib/ndk"; import { getNdkContext } from "$lib/ndk.ts";
const ndk = getNdkContext(); const ndk = getNdkContext();
Loading…
Cancel
Save