Browse Source

Editor

master
Nuša Pukšič 6 months ago committed by buttercat1791
parent
commit
f8cc80865d
  1. 6
      src/app.css
  2. 1
      src/lib/a/cards/AProfilePreview.svelte
  3. 145
      src/lib/a/forms/ACommentForm.svelte
  4. 101
      src/lib/a/forms/AMarkupForm.svelte
  5. 158
      src/lib/a/forms/ATextareaWithPreview.svelte
  6. 4
      src/lib/a/index.ts
  7. 2
      src/lib/a/nav/ANavbar.svelte
  8. 7
      src/lib/a/reader/AReaderPage.svelte
  9. 70
      src/lib/a/reader/AReaderTOC.svelte
  10. 47
      src/lib/a/reader/AReaderToolbar.svelte
  11. 32
      src/lib/a/reader/ATocNode.svelte
  12. 1
      src/lib/a/reader/scroll-spy.ts
  13. 4
      src/lib/a/reader/toc-utils.ts
  14. 90
      src/lib/components/EventDetails.svelte
  15. 24
      src/lib/components/publications/Publication.svelte
  16. 2
      src/lib/components/publications/PublicationFeed.svelte
  17. 4
      src/lib/components/publications/TableOfContents.svelte
  18. 15
      src/lib/components/util/Profile.svelte
  19. 8
      src/routes/+layout.svelte
  20. 1
      src/routes/about/+page.svelte
  21. 395
      src/routes/contact/+page.svelte

6
src/app.css

@ -598,7 +598,7 @@ @@ -598,7 +598,7 @@
/* Footnotes */
.footnote-ref {
text-decoration: none;
color: var(--color-primary);
color: var(--color-primary-500);
}
.footnotes {
@ -628,12 +628,12 @@ @@ -628,12 +628,12 @@
.footnote-backref {
text-decoration: none;
margin-left: 0.5rem;
color: var(--color-primary);
color: var(--color-primary-500);
}
.note-leather .footnote-ref,
.note-leather .footnote-backref {
color: var(--color-primary);
color: var(--color-primary-500);
}
/* Scrollable content */

1
src/lib/a/cards/AProfilePreview.svelte

@ -15,7 +15,6 @@ @@ -15,7 +15,6 @@
import { neventEncode, naddrEncode, nprofileEncode } from '$lib/utils';
import { isPubkeyInUserLists, fetchCurrentUserLists } from '$lib/utils/user_lists';
import type { NDKEvent } from '@nostr-dev-kit/ndk';
import { UserOutline } from 'flowbite-svelte-icons';
type UserLite = { npub?: string | null };
type Profile = {

145
src/lib/a/forms/ACommentForm.svelte

@ -1,14 +1,10 @@ @@ -1,14 +1,10 @@
<script lang="ts">
import { Textarea, Toolbar, ToolbarGroup, ToolbarButton, Button, Label, P } from "flowbite-svelte";
import {
Bold, Italic, Strikethrough,
Quote, Link2, Image, Hash,
List, ListOrdered
} from "@lucide/svelte";
import { Button, Label } from "flowbite-svelte";
import { userStore } from "$lib/stores/userStore.ts";
import { parseBasicMarkup } from "$lib/utils/markup/basicMarkupParser.ts";
import { basicMarkup } from "$lib/snippets/MarkupSnippets.svelte";
import { getNdkContext } from "$lib/ndk.ts";
import { ATextareaWithPreview } from "$lib/a/index.ts";
const ndk = getNdkContext();
@ -25,43 +21,6 @@ @@ -25,43 +21,6 @@
onSubmit?: (content: string) => Promise<void>;
}>();
let preview = $state("");
const markupButtons = [
{ label: "Bold", icon: Bold, action: () => insertMarkup("**", "**") },
{ label: "Italic", icon: Italic, action: () => insertMarkup("_", "_") },
{ label: "Strike", icon: Strikethrough, action: () => insertMarkup("~~", "~~") },
{ label: "Link", icon: Link2, action: () => insertMarkup("[", "](url)") },
{ label: "Image", icon: Image, action: () => insertMarkup("![", "](url)") },
{ label: "Quote", icon: Quote, action: () => insertMarkup("> ", "") },
{ label: "List", icon: List, action: () => insertMarkup("* ", "") },
{ label: "Numbered List", icon: ListOrdered, action: () => insertMarkup("1. ", "") },
{ label: "Hashtag", icon: Hash, action: () => insertMarkup("#", "") },
];
function insertMarkup(prefix: string, suffix: string) {
const textarea = document.querySelector("textarea");
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = content.substring(start, end);
content =
content.substring(0, start) +
prefix +
selectedText +
suffix +
content.substring(end);
// Set cursor position after the inserted markup
setTimeout(() => {
textarea.focus();
textarea.selectionStart = textarea.selectionEnd =
start + prefix.length + selectedText.length + suffix.length;
}, 0);
}
function clearForm() {
content = "";
}
@ -83,76 +42,38 @@ @@ -83,76 +42,38 @@
e.preventDefault();
await onSubmit(content.trim());
}
$effect(() => {
console.log("Content changed, updating preview...");
if (content.trim() === "") {
preview = "";
return;
}
parseBasicMarkup(content).then((html) => {
preview = html;
});
});
</script>
<form novalidate
onsubmit={handleSubmit}>
<form novalidate onsubmit={handleSubmit}>
<Label for="editor" class="sr-only">Comment</Label>
<Textarea id="editor" rows={10}
bind:value={content}
class="!m-0 p-4"
innerClass="!m-0 !bg-transparent"
headerClass="!m-0 !bg-transparent"
footerClass="!m-0 !bg-transparent"
addonClass="!m-0 top-3 hidden md:flex"
textareaClass="!m-0 !bg-transparent !border-0 !rounded-none !shadow-none !focus:ring-0"
placeholder="Write a comment">
{#snippet header()}
<Toolbar embedded class="flex-row !m-0">
<ToolbarGroup class="flex-row flex-wrap !m-0">
{#each markupButtons as button}
{#if button.icon}
{@const TheIcon = button.icon}
<ToolbarButton title={button.label} color="dark" size="md" onclick={button.action} >
<TheIcon size={24} />
</ToolbarButton>
{:else}
<ToolbarButton onclick={button.action}>{button.label}</ToolbarButton>
{/if}
{/each}
{@render extensions()}
</ToolbarGroup>
</Toolbar>
{/snippet}
{#snippet footer()}
<div class="flex flex-row justify-between">
<div class="flex flex-row flex-wrap gap-3 !m-0">
<Button size="xs" color="alternative" onclick={removeFormatting} class="!m-0">Remove Formatting</Button>
<Button size="xs" color="alternative" class="!m-0" onclick={clearForm}>Clear</Button>
</div>
<Button
disabled={isSubmitting || !content.trim() || !$userStore.signedIn}
type="submit">
{#if !$userStore.signedIn}
Not Signed In
{:else if isSubmitting}
Publishing...
{:else}
Post Comment
{/if}
</Button>
</div>
{/snippet}
</Textarea>
</form>
<div class="prose dark:prose-invert max-w-none p-4 border border-primary-500 border-s-4 rounded-lg">
{#if preview}
{@render basicMarkup(preview, ndk)}
{:else}
<P class="text-xs text-gray-500">Preview will appear here...</P>
{/if}
</div>
<ATextareaWithPreview
id="editor"
label=""
rows={10}
bind:value={content}
placeholder="Write a comment"
parser={parseBasicMarkup}
previewRenderer={(html) => basicMarkup(html, ndk)}
{extensions}
/>
<div class="flex flex-row justify-between mt-2">
<div class="flex flex-row flex-wrap gap-3 !m-0">
<Button size="xs" color="alternative" onclick={removeFormatting} class="!m-0">Remove Formatting</Button>
<Button size="xs" color="alternative" class="!m-0" onclick={clearForm}>Clear</Button>
</div>
<Button
disabled={isSubmitting || !content.trim() || !$userStore.signedIn}
type="submit"
>
{#if !$userStore.signedIn}
Not Signed In
{:else if isSubmitting}
Publishing...
{:else}
Post Comment
{/if}
</Button>
</div>
</form>

101
src/lib/a/forms/AMarkupForm.svelte

@ -0,0 +1,101 @@ @@ -0,0 +1,101 @@
<script lang="ts">
import { Label, Input, Button, Modal } from "flowbite-svelte";
import { parseAdvancedmarkup } from "$lib/utils/markup/advancedMarkupParser";
import { ATextareaWithPreview } from "$lib/a/index.ts";
let {
subject = $bindable(""),
content = $bindable(""),
isSubmitting = false,
clearSignal = 0,
onSubmit = async (_subject: string, _content: string) => {},
} = $props<{
subject?: string;
content?: string;
isSubmitting?: boolean;
clearSignal?: number;
onSubmit?: (subject: string, content: string) => Promise<void> | void;
}>();
// Local UI state
let showConfirmDialog = $state(false);
// Track last clear signal to avoid clearing on mount if default matches
let _lastClearSignal = $state<number | null>(null);
$effect(() => {
if (clearSignal !== _lastClearSignal) {
if (_lastClearSignal !== null) {
subject = "";
content = "";
}
_lastClearSignal = clearSignal;
}
});
function clearForm() {
subject = "";
content = "";
}
function handleSubmit(e: Event) {
e.preventDefault();
showConfirmDialog = true;
}
async function confirmSubmit() {
showConfirmDialog = false;
await onSubmit(subject.trim(), content.trim());
}
function cancelSubmit() {
showConfirmDialog = false;
}
</script>
<form class="space-y-4" onsubmit={handleSubmit} autocomplete="off">
<div>
<Label for="subject" class="mb-2">Subject</Label>
<Input
id="subject"
class="w-full"
placeholder="Issue subject"
bind:value={subject}
required
autofocus
/>
</div>
<div class="relative">
<ATextareaWithPreview
id="content"
label="Description"
bind:value={content}
placeholder="Describe your issue. Use the Eye toggle to preview rendered markup."
parser={parseAdvancedmarkup}
/>
</div>
<div class="flex justify-end space-x-4">
<Button type="button" color="alternative" onclick={clearForm}>Clear Form</Button>
<Button type="submit" tabindex={0} disabled={isSubmitting}>
{#if isSubmitting}
Submitting...
{:else}
Submit Issue
{/if}
</Button>
</div>
</form>
<!-- Confirmation Dialog -->
<Modal bind:open={showConfirmDialog} size="sm" autoclose={false} class="w-full">
<div class="text-center">
<h3 class="mb-5 text-lg font-normal text-gray-700 dark:text-gray-300">
Would you like to submit the issue?
</h3>
<div class="flex justify-center gap-4">
<Button color="alternative" onclick={cancelSubmit}>Cancel</Button>
<Button color="primary" onclick={confirmSubmit}>Submit</Button>
</div>
</div>
</Modal>

158
src/lib/a/forms/ATextareaWithPreview.svelte

@ -0,0 +1,158 @@ @@ -0,0 +1,158 @@
<script lang="ts">
import { Textarea, Toolbar, ToolbarGroup, ToolbarButton, Label, Button } from "flowbite-svelte";
import { Bold, Italic, Strikethrough, Quote, Link2, Image, Hash, List, ListOrdered, Eye, EyeClosed } from "@lucide/svelte";
// Reusable editor with toolbar (from ACommentForm) and toolbar-only Preview
let {
value = $bindable(""),
id = "editor",
label = "",
rows = 10,
placeholder = "",
// async parser that returns HTML string
parser = async (s: string) => s,
// optional snippet renderer (e.g., (html) => basicMarkup(html, ndk))
previewRenderer,
// extra toolbar extensions (snippet returning toolbar buttons)
extensions,
} = $props<{
value?: string;
id?: string;
label?: string;
rows?: number;
placeholder?: string;
parser?: (s: string) => Promise<string> | string;
previewRenderer?: (html: string) => any;
extensions?: any;
}>();
let preview = $state("");
let activeTab = $state<"write" | "preview">("write");
let wrapper: HTMLElement | null = null;
let isExpanded = $state(false);
const markupButtons = [
{ label: "Bold", icon: Bold, action: () => insertMarkup("**", "**") },
{ label: "Italic", icon: Italic, action: () => insertMarkup("_", "_") },
{ label: "Strike", icon: Strikethrough, action: () => insertMarkup("~~", "~~") },
{ label: "Link", icon: Link2, action: () => insertMarkup("[", "](url)") },
{ label: "Image", icon: Image, action: () => insertMarkup("![", "](url)") },
{ label: "Quote", icon: Quote, action: () => insertMarkup("> ", "") },
{ label: "List", icon: List, action: () => insertMarkup("* ", "") },
{ label: "Numbered List", icon: ListOrdered, action: () => insertMarkup("1. ", "") },
{ label: "Hashtag", icon: Hash, action: () => insertMarkup("#", "") },
];
function insertMarkup(prefix: string, suffix: string) {
const textarea = wrapper?.querySelector("textarea") as HTMLTextAreaElement | null;
if (!textarea) return;
const start = textarea.selectionStart;
const end = textarea.selectionEnd;
const selectedText = value.substring(start, end);
value = value.substring(0, start) + prefix + selectedText + suffix + value.substring(end);
// Set cursor position after the inserted markup
setTimeout(() => {
textarea.focus();
textarea.selectionStart = textarea.selectionEnd = start + prefix.length + selectedText.length + suffix.length;
}, 0);
}
function togglePreview() {
activeTab = activeTab === "write" ? "preview" : "write";
}
function toggleSize() {
isExpanded = !isExpanded;
}
$effect(() => {
if (activeTab !== "preview") return;
const src = value.trim();
if (!src) {
preview = "";
return;
}
Promise.resolve(parser(src)).then((html) => {
preview = html || "";
});
});
</script>
{#if label}
<Label for={id} class="mb-2">{label}</Label>
{/if}
<div bind:this={wrapper} class="rounded-lg">
<div class="min-h-[180px] relative">
{#if activeTab === 'write'}
<div class="inset-0">
<Textarea
id={id}
rows={isExpanded ? 30 : rows}
bind:value={value}
class="!m-0 p-0 h-full"
innerClass="!m-0 !bg-transparent !dark:bg-transparent"
headerClass="!m-0 !bg-transparent !dark:bg-transparent"
footerClass="!m-0 !bg-transparent"
addonClass="!m-0 top-3 hidden md:flex"
textareaClass="!m-0 !bg-transparent !dark:bg-transparent !border-0 !rounded-none !shadow-none !focus:ring-0"
placeholder={placeholder}
>
{#snippet header()}
<Toolbar embedded class="flex-row !m-0 !dark:bg-transparent !bg-transparent">
<ToolbarGroup class="flex-row flex-wrap !m-0">
{#each markupButtons as button}
{@const TheIcon = button.icon}
<ToolbarButton title={button.label} color="dark" size="md" onclick={button.action}>
<TheIcon size={24} />
</ToolbarButton>
{/each}
{#if extensions}
{@render extensions()}
{/if}
<ToolbarButton title="Toggle preview" color="dark" size="md" onclick={togglePreview}>
<Eye size={24} />
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
{/snippet}
</Textarea>
<Button
type="button"
size="xs"
class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100"
color="light"
onclick={toggleSize}
>
{isExpanded ? "⌃" : "⌄"}
</Button>
</div>
{:else}
<div class="absolute rounded-lg inset-0 flex flex-col bg-white">
<div class="py-2 px-3 border-gray-200 dark:border-gray-500 border-b">
<Toolbar embedded class="flex-row !m-0 !dark:bg-transparent !bg-transparent">
<ToolbarGroup class="flex-row flex-wrap !m-0">
<ToolbarButton title="Back to editor" color="dark" size="md" onclick={togglePreview}>
<EyeClosed size={24} />
</ToolbarButton>
</ToolbarGroup>
</Toolbar>
</div>
<div class="flex-1 overflow-auto px-4 max-w-none bg-white dark:bg-gray-800 text-gray-800 dark:text-gray-100 prose-content markup-content rounded-b-lg">
{#if preview}
{#if previewRenderer}
{@render previewRenderer(preview)}
{:else}
{@html preview}
{/if}
{:else}
<p class="text-xs text-gray-500">Nothing to preview</p>
{/if}
</div>
</div>
{/if}
</div>
</div>

4
src/lib/a/index.ts

@ -2,11 +2,11 @@ export { default as AThemeToggleMini } from './primitives/AThemeToggleMini.svelt @@ -2,11 +2,11 @@ export { default as AThemeToggleMini } from './primitives/AThemeToggleMini.svelt
export { default as AAlert } from './primitives/AAlert.svelte';
export { default as APagination } from './primitives/APagination.svelte';
export { default as ATocNode } from './reader/ATocNode.svelte';
export { default as ANavbar } from './nav/ANavbar.svelte';
export { default as AFooter } from './nav/AFooter.svelte';
export { default as ACommentForm } from './forms/ACommentForm.svelte';
export { default as AMarkupForm } from './forms/AMarkupForm.svelte';
export { default as ATextareaWithPreview } from './forms/ATextareaWithPreview.svelte';
export { default as AProfilePreview } from './cards/AProfilePreview.svelte';

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

@ -56,8 +56,6 @@ @@ -56,8 +56,6 @@
<NavLi>
<AThemeToggleMini />
</NavLi>
<NavLi>
<DarkMode />
</NavLi>
</NavUl>
</Navbar>

7
src/lib/a/reader/AReaderPage.svelte

@ -1,7 +0,0 @@ @@ -1,7 +0,0 @@
<script lang="ts">
let { className = '' , content } = $props();
</script>
<article class="mx-auto max-w-measure font-reading leading-reading text-[1rem] {className}">
{@render content()}
</article>

70
src/lib/a/reader/AReaderTOC.svelte

@ -1,70 +0,0 @@ @@ -1,70 +0,0 @@
<script lang="ts">
import { onMount } from 'svelte';
import type { TocItem } from './toc-utils';
import { ATocNode } from "$lib/a";
let {
items = [] as TocItem[],
activeId = null as string | null,
collapsible = true,
expandDepth = 1,
class: className = '',
onnavigate = undefined as undefined | ((href: string) => void)
} = $props();
let expanded = $state(new Set<string>());
let parentOf = new Map<string, string | null>();
function mapParents(list: TocItem[], parent: string | null = null) {
for (const it of list) {
parentOf.set(it.id, parent);
if (it.children?.length) mapParents(it.children, it.id);
}
}
function initExpansion(list: TocItem[], depth = 0) {
for (const it of list) {
if (depth < expandDepth) expanded.add(it.id);
if (it.children?.length) initExpansion(it.children, depth + 1);
}
expanded = new Set(expanded);
}
function expandAncestors(id: string | null) {
if (!id) return;
let cur: string | null | undefined = id;
while (cur) {
expanded.add(cur);
cur = parentOf.get(cur) ?? null;
}
expanded = new Set(expanded);
}
onMount(() => {
initExpansion(items, 0);
expandAncestors(activeId);
if (activeId) {
const el = document.querySelector(`[data-toc-id="${activeId}"]`);
if (el instanceof HTMLElement) el.scrollIntoView({ block: 'nearest' });
}
});
function toggle(id: string) {
if (!collapsible) return;
if (expanded.has(id)) expanded.delete(id);
else expanded.add(id);
expanded = new Set(expanded);
}
const pad = (depth: number) => `padding-left: calc(var(--space-4) * ${Math.max(depth, 0)})`;
const onNavigate = (href: string) => onnavigate?.(href);
</script>
<nav aria-label="Table of contents" class={`text-sm ${className}`}>
<ul class="space-y-1 max-h-[calc(100vh-6rem)] overflow-auto pr-1">
{#each items as item (item.id)}
<ATocNode {item} depth={0} {activeId} {collapsible} {expanded} {toggle} {pad} {onNavigate} />
{/each}
</ul>
</nav>

47
src/lib/a/reader/AReaderToolbar.svelte

@ -1,47 +0,0 @@ @@ -1,47 +0,0 @@
<script lang="ts">
import { theme, setTheme } from '$lib/stores/themeStore.ts';
let size = 16;
let line = 1.7;
function applySize() {
document.documentElement.style.fontSize = size + 'px';
}
function incSize() {
size = Math.min(22, size + 1);
applySize();
}
function decSize() {
size = Math.max(14, size - 1);
applySize();
}
function incLine() {
line = Math.min(2, Math.round((line + 0.05) * 100) / 100);
document.documentElement.style.setProperty('--leading-reading', String(line));
}
function decLine() {
line = Math.max(1.4, Math.round((line - 0.05) * 100) / 100);
document.documentElement.style.setProperty('--leading-reading', String(line));
}
</script>
<div class="flex items-center gap-3 p-2 border-b border-muted/20 bg-surface sticky top-0 z-10">
<label class="text-sm opacity-70">Theme</label>
<select
class="h-9 px-2 rounded-md border border-muted/30 bg-surface"
bind:value={$theme}
on:change={(e) => setTheme((e.target as HTMLSelectElement).value)}
>
<option value="light">Light</option>
<option value="dark">Dark</option>
<option value="sepia">Sepia</option>
</select>
<div class="mx-2 h-6 w-px bg-muted/30" />
<label class="text-sm opacity-70">Text size</label>
<Button variant="outline" size="sm" on:click={decSize}>−</Button>
<span class="text-sm w-8 text-center">{size}px</span>
<Button variant="outline" size="sm" on:click={incSize}>+</Button>
<div class="mx-2 h-6 w-px bg-muted/30" />
<label class="text-sm opacity-70">Line height</label>
<Button variant="outline" size="sm" on:click={decLine}>−</Button>
<span class="text-sm w-10 text-center">{line}</span>
<Button variant="outline" size="sm" on:click={incLine}>+</Button>
</div>

32
src/lib/a/reader/ATocNode.svelte

@ -1,32 +0,0 @@ @@ -1,32 +0,0 @@
<script lang="ts">
import type { TocItem } from './toc-utils';
import TocNode from './ATocNode.svelte';
let { item, depth = 0, activeId = null as string | null, collapsible = true, expanded, toggle, pad, onNavigate }: { item: TocItem; depth?: number; activeId?: string|null; collapsible?: boolean; expanded: Set<string>; toggle: (id:string)=>void; pad: (depth:number)=>string; onNavigate: (href:string)=>void; } = $props();
const hasChildren = !!(item.children && item.children.length > 0);
let isOpen = $derived(expanded.has(item.id));
let isActive = $derived(activeId === item.id);
</script>
<li>
<div class="flex items-center gap-1 rounded-md hover:bg-primary/10" style={pad(depth)}>
{#if collapsible && hasChildren}
<button class="shrink-0 h-6 w-6 grid place-items-center rounded-md hover:bg-primary/10" aria-label={isOpen?`Collapse ${item.title}`:`Expand ${item.title}`} aria-expanded={isOpen} onclick={() => toggle(item.id)}>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class={isOpen ? 'rotate-90 transition-transform' : 'transition-transform'}><path d="M9 18l6-6-6-6" /></svg>
</button>
{:else}
<span class="shrink-0 h-6 w-6" />
{/if}
<a href={item.href ?? `#${item.id}`} data-toc-id={item.id}
class="flex-1 min-w-0 rounded-md px-2 py-1.5 hover:bg-primary/10 focus:outline-none focus:ring-2 focus:ring-primary/40"
class:text-primary={isActive}
onclick={() => onNavigate(item.href ?? `#${item.id}`)}>
<span class="truncate">{item.title}</span>
</a>
</div>
{#if hasChildren}
<ul id={`sub-${item.id}`} aria-hidden={!isOpen} class={isOpen ? 'mt-1 space-y-1' : 'hidden'}>
{#each item.children as child (child.id)}
<TocNode item={child} depth={depth + 1} {activeId} {collapsible} {expanded} {toggle} {pad} {onNavigate} />
{/each}
</ul>
{/if}
</li>

1
src/lib/a/reader/scroll-spy.ts

@ -1 +0,0 @@ @@ -1 +0,0 @@
import { writable } from 'svelte/store'; export function createScrollSpy(ids:string[], opts:{ rootMargin?:string; threshold?:number[] }={}){ const active=writable<string|null>(null); let observer:IntersectionObserver|null=null; function start(){ if(typeof window==='undefined' || typeof IntersectionObserver==='undefined') return; observer=new IntersectionObserver((entries)=>{ const visible=entries.filter(e=>e.isIntersecting).sort((a,b)=>a.target.getBoundingClientRect().top - b.target.getBoundingClientRect().top); if(visible[0]) active.set((visible[0].target as HTMLElement).id); }, { rootMargin: opts.rootMargin ?? '-30% 0px -60% 0px', threshold: opts.threshold ?? [0,1] }); for(const id of ids){ const el=document.getElementById(id); if(el) observer.observe(el); } } function stop(){ observer?.disconnect(); observer=null; } return { active, start, stop }; }

4
src/lib/a/reader/toc-utils.ts

@ -1,4 +0,0 @@ @@ -1,4 +0,0 @@
export type TocItem = { id:string; title:string; href?:string; children?: TocItem[]; };
const slugify=(s:string)=>s.toLowerCase().trim().replace(/[^\\w\\s-]/g,'').replace(/\\s+/g,'-').slice(0,80);
export function buildTocFromDocument(root:ParentNode=document, levels:number[]=[2,3,4]):TocItem[]{ const selector=levels.map(l=>`h${'${'}l}`).join(','); const nodes=Array.from(root.querySelectorAll(selector)) as HTMLElement[]; const toc:TocItem[]=[]; const stack:{level:number; item:TocItem}[]=[]; for(const el of nodes){ const level=Number(el.tagName.slice(1)); const text=(el.textContent||'').trim(); if(!text) continue; if(!el.id) el.id=slugify(text); const entry: TocItem = { id:el.id, title:text, href:'#'+el.id, children:[] }; while(stack.length && stack[stack.length-1].level>=level) stack.pop(); if(stack.length===0) toc.push(entry); else stack[stack.length-1].item.children!.push(entry); stack.push({level,item:entry}); } return toc; }
export function idsFromToc(items:TocItem[]):string[]{ const out:string[]=[]; const walk=(arr:TocItem[])=>{ for(const it of arr){ out.push(it.id); if(it.children?.length) walk(it.children); } }; walk(items); return out; }

90
src/lib/components/EventDetails.svelte

@ -262,12 +262,10 @@ @@ -262,12 +262,10 @@
// --- Identifier helpers ---
function getIdentifiers(
event: NDKEvent,
profile: any,
_profile: any,
): { label: string; value: string; link?: string }[] {
const ids: { label: string; value: string; link?: string }[] = [];
if (event.kind === 0) {
// NIP-05
const nip05 = profile?.nip05 || getMatchingTags(event, "nip05")[0]?.[1];
// npub
const npub = toNpub(event.pubkey);
if (npub)
@ -331,7 +329,7 @@ @@ -331,7 +329,7 @@
{#if toNpub(event.pubkey)}
<span class="text-gray-600 dark:text-gray-400 min-w-0"
>Author: {@render userBadge(
toNpub(event.pubkey) as string,
toNpub(event.pubkey) || '',
profile?.display_name || undefined,
ndk,
)}</span
@ -368,54 +366,54 @@ @@ -368,54 +366,54 @@
<div class="card-leather bg-highlight dark:bg-primary-800 p-4 mb-4 rounded-lg border max-w-full overflow-hidden">
<div class="flex flex-col space-y-1 min-w-0">
<span class="text-gray-700 dark:text-gray-300 font-semibold">Content:</span>
<div class="prose dark:prose-invert max-w-none text-gray-900 dark:text-gray-100 break-words overflow-wrap-anywhere min-w-0">
{#if isRepost}
<!-- Repost content handling -->
{#if repostKinds.includes(event.kind)}
<!-- Kind 6 and 16 reposts - stringified JSON content -->
<div class="border-l-4 border-primary-300 dark:border-primary-600 pl-3 mb-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
{event.kind === 6 ? 'Reposted content:' : 'Generic reposted content:'}
</div>
{@render repostContent(event.content)}
</div>
{:else if event.kind === 1 && event.getMatchingTags("q").length > 0}
<!-- Quote repost - kind 1 with q tag -->
<div class="border-l-4 border-primary-300 dark:border-primary-600 pl-3 mb-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
Quote repost:
<div class={shouldTruncate ? 'max-h-32 overflow-hidden' : ''}>
{#if isRepost}
<!-- Repost content handling -->
{#if repostKinds.includes(event.kind)}
<!-- Kind 6 and 16 reposts - stringified JSON content -->
<div class="border-l-4 border-primary-300 dark:border-primary-600 pl-3 mb-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
{event.kind === 6 ? 'Reposted content:' : 'Generic reposted content:'}
</div>
{@render repostContent(event.content)}
</div>
{@render quotedContent(event, [], ndk)}
{#if content}
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
Added comment:
</div>
{#if repostKinds.includes(kind)}
{@html content}
{:else}
{@render basicMarkup(content, ndk)}
{/if}
{:else if event.kind === 1 && event.getMatchingTags("q").length > 0}
<!-- Quote repost - kind 1 with q tag -->
<div class="border-l-4 border-primary-300 dark:border-primary-600 pl-3 mb-2">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
Quote repost:
</div>
{@render quotedContent(event, [], ndk)}
{#if content}
<div class="mt-3 pt-3 border-t border-gray-200 dark:border-gray-700">
<div class="text-xs text-gray-500 dark:text-gray-400 mb-1">
Added comment:
</div>
{#if repostKinds.includes(kind)}
{@html content}
{:else}
{@render basicMarkup(content, ndk)}
{/if}
</div>
{/if}
</div>
{/if}
{:else}
<!-- Regular content -->
<div class={shouldTruncate ? 'max-h-32 overflow-hidden' : ''}>
{#if repostKinds.includes(kind)}
{@html content}
{:else}
{@render basicMarkup(content, ndk)}
{/if}
</div>
{/if}
{:else}
<!-- Regular content -->
<div class={shouldTruncate ? 'max-h-32 overflow-hidden' : ''}>
{#if repostKinds.includes(kind)}
{@html content}
{:else}
{@render basicMarkup(content, ndk)}
{#if shouldTruncate}
<button
class="mt-2 text-primary-700 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-200"
onclick={() => (showFullContent = true)}>Show more</button
>
{/if}
</div>
{#if shouldTruncate}
<button
class="mt-2 text-primary-700 hover:text-primary-900 dark:text-primary-400 dark:hover:text-primary-200"
onclick={() => (showFullContent = true)}>Show more</button
>
{/if}
{/if}
</div>
</div>
</div>

24
src/lib/components/publications/Publication.svelte

@ -7,7 +7,7 @@ @@ -7,7 +7,7 @@
SidebarGroup,
SidebarWrapper,
Heading,
CloseButton,
CloseButton, uiHelpers
} from "flowbite-svelte";
import { getContext, onDestroy, onMount } from "svelte";
import {
@ -143,6 +143,10 @@ @@ -143,6 +143,10 @@
let currentBlogEvent: null | NDKEvent = $state(null);
const isLeaf = $derived(indexEvent.kind === 30041);
const tocSidebarUi = uiHelpers();
const closeTocSidebar = tocSidebarUi.close;
const isTocOpen = $state($publicationColumnVisibility.toc);
function isInnerActive() {
return currentBlog !== null && $publicationColumnVisibility.inner;
}
@ -249,16 +253,21 @@ @@ -249,16 +253,21 @@
<!-- Table of contents -->
{#if publicationType !== "blog" || !isLeaf}
{#if $publicationColumnVisibility.toc}
<Sidebar
isOpen={isTocOpen}
closeSidebar={closeTocSidebar}
class="z-50 h-full pt-6 ml-4 bg-transparent sticky top-[80px]"
backdrop={true}
activeUrl={`#${activeAddress ?? ""}`}
asideClass="fixed md:sticky top-[130px] sm:top-[146px] h-[calc(100vh-130px)] sm:h-[calc(100vh-146px)] z-10 bg-primary-50 dark:bg-primary-1000 px-5 w-80 left-0 pt-4 md:!pr-16 overflow-y-auto border border-l-4 rounded-lg border-primary-200 dark:border-primary-800 my-4"
activeClass="flex items-center p-2 bg-primary-50 dark:bg-primary-800 p-2 rounded-lg"
nonActiveClass="flex items-center p-2 hover:bg-primary-50 dark:hover:bg-primary-800 p-2 rounded-lg"
classes={{
div: 'bg-primary-50 dark:bg-primary-1000 border border-s-4 rounded border-primary-200 dark:border-primary-800',
active: 'bg-primary-100 dark:bg-primary-800 p-2 rounded-lg',
nonactive: 'bg-primary-50 dark:bg-primary-800',
}}
>
<CloseButton
onclick={closeToc}
class="btn-leather absolute top-4 right-4 hover:bg-primary-50 dark:hover:bg-primary-800"
onclick={closeTocSidebar}
class="btn-leather hover:bg-primary-50 dark:hover:bg-primary-800"
/>
<TableOfContents
{rootAddress}
@ -273,7 +282,6 @@ @@ -273,7 +282,6 @@
}}
/>
</Sidebar>
{/if}
{/if}
<!-- Default publications -->

2
src/lib/components/publications/PublicationFeed.svelte

@ -680,7 +680,7 @@ @@ -680,7 +680,7 @@
>
{#if loading && eventsInView.length === 0}
{#each getSkeletonIds() as id}
<Skeleton divClass="skeleton-leather w-full" size="lg" />
<Skeleton divClass="skeleton-leather w-full" size="lg" />
{/each}
{:else if eventsInView.length > 0}
{#each eventsInView as event}

4
src/lib/components/publications/TableOfContents.svelte

@ -166,7 +166,9 @@ @@ -166,7 +166,9 @@
spanClass="px-2 text-ellipsis"
class={`${isVisible ? "toc-highlight" : ""} ${isLastEntry ? "pb-4" : ""}`}
onclick={() => handleSectionClick(address)}
/>
>
<!-- Empty for now - could add icons or labels here in the future -->
</SidebarItem>
{:else}
{@const childDepth = depth + 1}
<SidebarDropdownWrapper

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

@ -1,15 +1,9 @@ @@ -1,15 +1,9 @@
<script lang="ts">
import CopyToClipboard from "$components/util/CopyToClipboard.svelte";
import NetworkStatus from "$components/NetworkStatus.svelte";
import {
logoutUser,
userStore,
loginWithExtension,
loginWithAmber,
loginWithNpub
} from "$lib/stores/userStore";
import { Avatar, Dropdown, DropdownGroup, DropdownItem, DropdownHeader } from "flowbite-svelte";
import { Globe, Loader, Book, Smartphone } from "@lucide/svelte";
import { loginWithAmber, loginWithExtension, loginWithNpub, logoutUser, userStore } from "$lib/stores/userStore";
import { Avatar, Dropdown, DropdownGroup, DropdownHeader, DropdownItem } from "flowbite-svelte";
import { Book, Globe, Loader, Smartphone } from "@lucide/svelte";
import { get } from "svelte/store";
import { goto } from "$app/navigation";
import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk";
@ -112,8 +106,7 @@ @@ -112,8 +106,7 @@
// Reset the refresh flag when user logs out
$effect(() => {
const currentUser = userState;
if (!currentUser.signedIn) {
if (!userState.signedIn) {
hasRefreshedProfile = false;
}
});

8
src/routes/+layout.svelte

@ -3,10 +3,10 @@ @@ -3,10 +3,10 @@
import { onMount, setContext } from "svelte";
import { goto } from "$app/navigation";
import { cleanupNdk, getPersistedLogin } from "$lib/ndk";
import { userStore, loginMethodStorageKey } from "$lib/stores/userStore";
import { loginMethodStorageKey, userStore } from "$lib/stores/userStore";
import type { LayoutProps } from "./$types";
import { page } from "$app/state";
import { ANavbar, AFooter } from "$lib/a/index.js";
import { AFooter, ANavbar } from "$lib/a/index.js";
// Define children prop for Svelte 5
let { data, children }: LayoutProps = $props();
@ -53,10 +53,8 @@ @@ -53,10 +53,8 @@
if (persistedPubkey && loginMethod) {
console.log("Layout: Found persisted authentication, attempting to restore...");
const currentUserState = $userStore;
// Only restore if not already signed in
if (!currentUserState.signedIn) {
if (!$userStore.signedIn) {
console.log("Layout: User not currently signed in, restoring authentication...");
if (loginMethod === "extension") {

1
src/routes/about/+page.svelte

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import { Heading, Img, P, A } from "flowbite-svelte";
import { goto } from "$app/navigation";
import RelayStatus from "$lib/components/RelayStatus.svelte";
import { getNdkContext } from "$lib/ndk";
// Get the git tag version from environment variables

395
src/routes/contact/+page.svelte

@ -1,14 +1,5 @@ @@ -1,14 +1,5 @@
<script lang="ts">
import {
Heading,
P,
A,
Button,
Label,
Textarea,
Input,
Modal,
} from "flowbite-svelte";
import { Heading, P, A } from "flowbite-svelte";
import { activeInboxRelays, activeOutboxRelays, getNdkContext } from "$lib/ndk";
import { userStore } from "$lib/stores/userStore";
import { anonymousRelays } from "$lib/consts";
@ -20,6 +11,8 @@ @@ -20,6 +11,8 @@
import { nip19 } from "nostr-tools";
import { getMimeTags } from "$lib/utils/mime";
import { userBadge } from "$lib/snippets/UserSnippets.svelte";
import AMarkupForm from "$lib/a/forms/AMarkupForm.svelte";
import { AAlert } from "$lib/a";
const ndk = getNdkContext();
@ -33,8 +26,6 @@ @@ -33,8 +26,6 @@
subject = "";
content = "";
submissionError = "";
isExpanded = false;
activeTab = "write";
}
let subject = $state("");
@ -46,9 +37,6 @@ @@ -46,9 +37,6 @@
let submittedEvent = $state<NDKEvent | null>(null);
let issueLink = $state("");
let successfulRelays = $state<string[]>([]);
let isExpanded = $state(false);
let activeTab = $state("write");
let showConfirmDialog = $state(false);
// Store form data when user needs to login
let savedFormData = {
@ -82,45 +70,28 @@ @@ -82,45 +70,28 @@
return url.replace(/\/+$/, "");
}
function toggleSize() {
isExpanded = !isExpanded;
}
async function handleSubmit(e: Event) {
// Prevent form submission
e.preventDefault();
/**
* Handle form submission from AMarkupForm
*/
async function handleFormSubmit(newSubject: string, newContent: string) {
submissionError = "";
subject = newSubject;
content = newContent;
if (!subject || !content) {
submissionError = "Please fill in all fields";
return;
}
// Check if user is logged in
if (!user.signedIn) {
// Save form data
savedFormData = {
subject,
content,
};
// Show login modal
savedFormData = { subject, content };
showLoginModal = true;
return;
}
// Show confirmation dialog
showConfirmDialog = true;
}
async function confirmSubmit() {
showConfirmDialog = false;
await submitIssue();
}
function cancelSubmit() {
showConfirmDialog = false;
}
/**
* Publish event to relays with retry logic
*/
@ -321,273 +292,115 @@ @@ -321,273 +292,115 @@
an issue, that will appear on our repo page.
</P>
<form class="space-y-4" onsubmit={handleSubmit} autocomplete="off">
<div>
<Label for="subject" class="mb-2">Subject</Label>
<Input
id="subject"
class="w-full bg-white dark:bg-gray-800"
placeholder="Issue subject"
bind:value={subject}
required
autofocus
/>
</div>
<div class="relative">
<Label for="content" class="mb-2">Description</Label>
<div
class="relative border border-gray-300 dark:border-gray-600 rounded-lg {isExpanded
? 'h-[800px]'
: 'h-[200px]'} transition-all duration-200 sm:w-[95vw] md:w-full"
<AMarkupForm
bind:subject={subject}
bind:content={content}
isSubmitting={isSubmitting}
onSubmit={handleFormSubmit}
/>
{#if submissionSuccess && submittedEvent}
<div
class="p-6 mb-4 text-sm bg-success-200 dark:bg-success-700 border border-success-300 dark:border-success-600 rounded-lg relative"
role="alert"
>
<!-- Close button -->
<button
class="absolute top-2 right-2 text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100"
onclick={closeSuccessMessage}
aria-label="Close"
>
<div class="h-full flex flex-col">
<div
class="border-b border-gray-300 dark:border-gray-600 rounded-t-lg"
>
<ul
class="flex flex-wrap -mb-px text-sm font-medium text-center"
role="tablist"
>
<li class="mr-2" role="presentation">
<button
type="button"
class="inline-block p-4 rounded-t-lg {activeTab === 'write'
? 'border-b-2 border-primary-600 text-primary-600'
: 'hover:text-gray-600 hover:border-gray-300'}"
onclick={() => (activeTab = "write")}
role="tab"
>
Write
</button>
</li>
<li role="presentation">
<button
type="button"
class="inline-block p-4 rounded-t-lg {activeTab ===
'preview'
? 'border-b-2 border-primary-600 text-primary-600'
: 'hover:text-gray-600 hover:border-gray-300'}"
onclick={() => (activeTab = "preview")}
role="tab"
>
Preview
</button>
</li>
</ul>
</div>
<div class="flex-1 min-h-0 relative">
{#if activeTab === "write"}
<div class="absolute inset-0 overflow-hidden">
<Textarea
id="content"
class="w-full h-full resize-none bg-primary-50 dark:bg-primary-1000 text-gray-900 dark:text-gray-100 border-s-4 border-primary-200 rounded-b-lg rounded-t-none shadow-none px-4 py-2 focus:border-primary-600 dark:focus:border-primary-400"
bind:value={content}
required
placeholder="Describe your issue in detail...
The following markup is supported:
# Headers (1-6 levels)
Header 1
======
*Bold* or **bold**
_Italic_ or __italic__ text
~Strikethrough~ or ~~strikethrough~~ text
> Blockquotes
Lists, including nested:
* Bullets/unordered lists
1. Numbered/ordered lists
[Links](url)
![Images](url)
`Inline code`
```language
Code blocks with syntax highlighting for over 100 languages
```
| Tables | With or without headers |
|--------|------|
| Multiple | Rows |
Footnotes[^1] and [^1]: footnote content
Also renders nostr identifiers: npubs, nprofiles, nevents, notes, and naddrs. With or without the nostr: prefix."
/>
</div>
{:else}
<div
class="absolute inset-0 p-4 max-w-none bg-white dark:bg-gray-800 prose-content markup-content"
>
{#key content}
{#await parseAdvancedmarkup(content)}
<p>Loading preview...</p>
{:then html}
{@html html ||
'<p class="text-gray-700 dark:text-gray-300">Nothing to preview</p>'}
{:catch error}
<p class="text-red-500">
Error rendering preview: {error.message}
</p>
{/await}
{/key}
</div>
{/if}
</div>
</div>
<Button
type="button"
size="xs"
class="absolute bottom-2 right-2 z-10 opacity-60 hover:opacity-100"
color="light"
onclick={toggleSize}
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
<div class="flex items-center mb-3">
<svg
class="w-5 h-5 mr-2 text-success-700 dark:text-success-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
></path>
</svg>
<span class="font-medium text-success-800 dark:text-success-200"
>Issue submitted successfully!</span
>
{isExpanded ? "⌃" : "⌄"}
</Button>
</div>
</div>
<div class="flex justify-end space-x-4">
<Button type="button" color="alternative" onclick={clearForm}>
Clear Form
</Button>
<Button type="submit" tabindex={0}>
{#if isSubmitting}
Submitting...
{:else}
Submit Issue
{/if}
</Button>
</div>
{#if submissionSuccess && submittedEvent}
<div
class="p-6 mb-4 text-sm bg-success-200 dark:bg-success-700 border border-success-300 dark:border-success-600 rounded-lg relative"
role="alert"
class="mb-3 p-3 bg-white dark:bg-gray-800 rounded border border-success-200 dark:border-success-600"
>
<!-- Close button -->
<button
class="absolute top-2 right-2 text-gray-700 hover:text-gray-900 dark:text-gray-300 dark:hover:text-gray-100"
onclick={closeSuccessMessage}
aria-label="Close"
>
<svg
class="w-5 h-5"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
<div class="mb-2">
<span class="font-semibold">Subject:</span>
<span
>{submittedEvent.tags.find((t) => t[0] === "subject")?.[1] ||
"No subject"}</span
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M6 18L18 6M6 6l12 12"
></path>
</svg>
</button>
<div class="flex items-center mb-3">
<svg
class="w-5 h-5 mr-2 text-success-700 dark:text-success-300"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
clip-rule="evenodd"
></path>
</svg>
<span class="font-medium text-success-800 dark:text-success-200"
>Issue submitted successfully!</span
>
</div>
<div
class="mb-3 p-3 bg-white dark:bg-gray-800 rounded border border-success-200 dark:border-success-600"
>
<div class="mb-2">
<span class="font-semibold">Subject:</span>
<span
>{submittedEvent.tags.find((t) => t[0] === "subject")?.[1] ||
"No subject"}</span
>
</div>
<div>
<span class="font-semibold">Description:</span>
<div class="mt-1 note-leather max-h-[400px] overflow-y-auto">
{#await parseAdvancedmarkup(submittedEvent.content)}
<p>Loading...</p>
{:then html}
{@html html}
{:catch error}
<p class="text-red-500">
Error rendering markup: {error.message}
</p>
{/await}
</div>
</div>
</div>
<div class="mb-3">
<span class="font-semibold">View your issue:</span>
<div class="mt-1">
<A
href={issueLink}
target="_blank"
class="hover:underline text-primary-600 dark:text-primary-500 break-all"
>
{issueLink}
</A>
<div>
<span class="font-semibold">Description:</span>
<div class="mt-1 note-leather max-h-[400px] overflow-y-auto">
{#await parseAdvancedmarkup(submittedEvent.content)}
<p>Loading...</p>
{:then html}
{@html html}
{:catch error}
<p class="text-red-500">
Error rendering markup: {error.message}
</p>
{/await}
</div>
</div>
</div>
<!-- Display successful relays -->
<div class="text-sm">
<span class="font-semibold">Successfully published to relays:</span>
<ul class="list-disc list-inside mt-1">
{#each successfulRelays as relay}
<li class="text-success-700 dark:text-success-300">{relay}</li>
{/each}
</ul>
<div class="mb-3">
<span class="font-semibold">View your issue:</span>
<div class="mt-1">
<A
href={issueLink}
target="_blank"
class="hover:underline text-primary-600 dark:text-primary-500 break-all"
>
{issueLink}
</A>
</div>
</div>
{/if}
{#if submissionError}
<div
class="p-4 mb-4 text-sm text-red-700 bg-red-100 rounded-lg"
role="alert"
>
{submissionError}
<!-- Display successful relays -->
<div class="text-sm">
<span class="font-semibold">Successfully published to relays:</span>
<ul class="list-disc list-inside mt-1">
{#each successfulRelays as relay}
<li class="text-success-700 dark:text-success-300">{relay}</li>
{/each}
</ul>
</div>
{/if}
</form>
</div>
</div>
{/if}
<!-- Confirmation Dialog -->
<Modal bind:open={showConfirmDialog} size="sm" autoclose={false} class="w-full">
<div class="text-center">
<h3 class="mb-5 text-lg font-normal text-gray-700 dark:text-gray-300">
Would you like to submit the issue?
</h3>
<div class="flex justify-center gap-4">
<Button color="alternative" onclick={cancelSubmit}>Cancel</Button>
<Button color="primary" onclick={confirmSubmit}>Submit</Button>
</div>
</div>
</Modal>
{#if submissionError}
<AAlert color="red">
{submissionError}
</AAlert>
{/if}
</div>
<!-- Login Modal -->
<LoginModal

Loading…
Cancel
Save