🔄 ✨ Forum overhaul: Phora → Arbor (#1)
🔧 Replace legacy system with NIP-BB implementation 🚀 Enhance forum user experience with improved navigation and interactions 🎨 Redesign UI for forum 🏷️ Rebrand from "Phora" to "Arbor"
This commit is contained in:
parent
2a1447cd77
commit
5afeb4d01a
19 changed files with 1233 additions and 610 deletions
202
src/routes/Arbor/Home.ts
Normal file
202
src/routes/Arbor/Home.ts
Normal file
|
@ -0,0 +1,202 @@
|
|||
import { getSigner, ndk } from '@/ndk';
|
||||
import { NDKEvent, type NDKKind, type NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
import '@components/Breadcrumbs';
|
||||
import '@components/Arbor/ForumTopic';
|
||||
import '@components/Arbor/ForumCategory';
|
||||
import '@components/Arbor/Button';
|
||||
import '@components/Prompt';
|
||||
|
||||
import type { EvePrompt } from '@components/Prompt';
|
||||
import { map } from 'lit/directives/map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
interface ForumTopic {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ForumCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
topics: ForumTopic[];
|
||||
}
|
||||
|
||||
@customElement('arx-arbor-home')
|
||||
export class ArborForum extends LitElement {
|
||||
@state()
|
||||
private categories: ForumCategory[] = [];
|
||||
|
||||
private topicsQuery: NDKSubscription | undefined;
|
||||
|
||||
@state()
|
||||
private forum: NDKEvent | null = null;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadCategories();
|
||||
}
|
||||
|
||||
newCategoryButtonClicked() {
|
||||
const prompt = this.shadowRoot?.getElementById('new-category-prompt') as EvePrompt;
|
||||
if (prompt) prompt.show();
|
||||
}
|
||||
|
||||
private async doCreateCategory() {
|
||||
if (!this.forum) return;
|
||||
const prompt = this.shadowRoot?.getElementById('new-category-prompt') as EvePrompt;
|
||||
const newCategory = prompt.getValue();
|
||||
|
||||
if (newCategory.length < 3) {
|
||||
alert('Category name must be at least 3 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await getSigner();
|
||||
const dtag = (Date.now() + Math.floor(Math.random() * 1000)).toString(32);
|
||||
const newForum = new NDKEvent(ndk);
|
||||
newForum.kind = 60890;
|
||||
newForum.tags = [...this.forum.tags, ['forum', `60891:${dtag}`, newCategory]];
|
||||
newForum.content = this.forum.content;
|
||||
await newForum.sign();
|
||||
await newForum.publish();
|
||||
this.forum = newForum;
|
||||
|
||||
const categoryEvent = new NDKEvent(ndk);
|
||||
categoryEvent.kind = 60891;
|
||||
categoryEvent.tags = [
|
||||
['d', dtag],
|
||||
['name', newCategory],
|
||||
];
|
||||
categoryEvent.content = '';
|
||||
await categoryEvent.sign();
|
||||
await categoryEvent.publish();
|
||||
|
||||
this.categories = [
|
||||
...this.categories,
|
||||
{
|
||||
id: dtag,
|
||||
name: newCategory,
|
||||
topics: [],
|
||||
},
|
||||
];
|
||||
|
||||
this.loadTopics();
|
||||
} catch (error) {
|
||||
console.error('Failed to create category:', error);
|
||||
alert('Failed to create category');
|
||||
}
|
||||
}
|
||||
|
||||
private async loadCategories() {
|
||||
await getSigner();
|
||||
this.forum = await ndk.fetchEvent({
|
||||
kinds: [60890 as NDKKind],
|
||||
'#d': ['arbor'],
|
||||
});
|
||||
if (!this.forum) {
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 60890;
|
||||
event.tags.push(['d', 'arbor']);
|
||||
event.content = '';
|
||||
await event.sign();
|
||||
await event.publish();
|
||||
this.forum = event;
|
||||
}
|
||||
for (const tag of this.forum.tags) {
|
||||
if (tag[0] === 'forum') {
|
||||
const [_, categoryId] = tag[1].split(':');
|
||||
this.categories = [
|
||||
...this.categories,
|
||||
{
|
||||
id: categoryId,
|
||||
name: tag[2],
|
||||
topics: [],
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
this.loadTopics();
|
||||
}
|
||||
|
||||
private async loadTopics() {
|
||||
if (this.topicsQuery) this.topicsQuery.stop();
|
||||
this.topicsQuery = ndk
|
||||
.subscribe({
|
||||
kinds: [892 as NDKKind],
|
||||
'#d': this.categories.map((category) => `60891:${category.id}`),
|
||||
})
|
||||
.on('event', (event) => {
|
||||
const categoryId = this.categories.findIndex(
|
||||
(category) => category.id === event.tags.find((tag) => tag[0] === 'd')?.[1].split(':')[1],
|
||||
);
|
||||
if (categoryId === -1) return;
|
||||
this.categories[categoryId].topics = [
|
||||
...this.categories[categoryId].topics,
|
||||
{
|
||||
id: event.id,
|
||||
title: event.tags.find((tag) => tag[0] === 'name')?.[1] || '',
|
||||
author: event.pubkey,
|
||||
description: event.content,
|
||||
created_at: new Date((event.created_at || 0) * 1000).toString(),
|
||||
},
|
||||
];
|
||||
this.categories = [...this.categories];
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<arx-breadcrumbs
|
||||
.items=${[{ text: 'Home', href: '/' }, { text: 'Arbor' }]}
|
||||
>
|
||||
</arx-breadcrumbs>
|
||||
|
||||
<arx-arbor-button @click=${this.newCategoryButtonClicked}>
|
||||
New Category
|
||||
</arx-arbor-button>
|
||||
|
||||
<arx-prompt
|
||||
id="new-category-prompt"
|
||||
promptText="What would you like to call this category?"
|
||||
showInput
|
||||
placeholder="New Category"
|
||||
@save=${this.doCreateCategory}
|
||||
></arx-prompt>
|
||||
|
||||
${map(
|
||||
this.categories,
|
||||
(category) => html`
|
||||
<arx-arbor-forum-category id=${category.id} title=${category.name}>
|
||||
${when(category.topics.length === 0, () => html` <div style="padding: 1rem 1.5rem">No topics...</div> `)}
|
||||
${map(
|
||||
category.topics,
|
||||
(topic) => html`
|
||||
<arx-arbor-forum-topic
|
||||
id=${topic.id}
|
||||
title=${topic.title}
|
||||
description=${topic.description}
|
||||
lastPostBy=${topic.author}
|
||||
lastPostTime=${topic.created_at}
|
||||
>
|
||||
</arx-arbor-forum-topic>
|
||||
`,
|
||||
)}
|
||||
</arx-arbor-forum-category>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -3,14 +3,17 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-phora-post-creator')
|
||||
export class PhoraPostCreator extends LitElement {
|
||||
@customElement('arx-arbor-post-creator')
|
||||
export class ArborPostCreator extends LitElement {
|
||||
@property({ type: String })
|
||||
topicId = '';
|
||||
|
||||
@state()
|
||||
private postContent = '';
|
||||
|
||||
@state()
|
||||
private topic: NDKEvent | null = null;
|
||||
|
||||
@state()
|
||||
private isCreating = false;
|
||||
|
||||
|
@ -67,8 +70,26 @@ export class PhoraPostCreator extends LitElement {
|
|||
}
|
||||
`;
|
||||
|
||||
override connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.loadTopic();
|
||||
}
|
||||
|
||||
private async loadTopic() {
|
||||
try {
|
||||
await getSigner();
|
||||
this.topic = await ndk.fetchEvent(this.topicId);
|
||||
if (!this.topic) {
|
||||
throw new Error('Could not load topic');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load topic:', error);
|
||||
}
|
||||
}
|
||||
|
||||
private async doCreatePost() {
|
||||
if (this.isCreating) return;
|
||||
if (!this.topic) return;
|
||||
|
||||
if (this.postContent.length < 10) {
|
||||
this.error = 'Post content must be at least 10 characters long';
|
||||
|
@ -78,26 +99,18 @@ export class PhoraPostCreator extends LitElement {
|
|||
this.error = null;
|
||||
this.isCreating = true;
|
||||
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 893;
|
||||
event.tags = [
|
||||
['A', this.topic.tags.find((tag) => tag[0] === 'd')?.[1] || ''],
|
||||
['E', this.topicId],
|
||||
['p', this.topic.pubkey],
|
||||
];
|
||||
event.content = this.postContent;
|
||||
|
||||
try {
|
||||
await getSigner();
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 1111;
|
||||
event.tags = [['e', this.topicId]];
|
||||
event.content = this.postContent;
|
||||
await event.sign();
|
||||
await event.publish();
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('post-created', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
detail: {
|
||||
postId: event.id,
|
||||
topicId: this.topicId,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset form
|
||||
this.postContent = '';
|
||||
} catch (error) {
|
||||
|
@ -130,19 +143,19 @@ export class PhoraPostCreator extends LitElement {
|
|||
${this.error ? html` <div class="error">${this.error}</div> ` : null}
|
||||
|
||||
<div class="actions">
|
||||
<arx-phora-button
|
||||
<arx-arbor-button
|
||||
@click=${() => this.dispatchEvent(new CustomEvent('cancel'))}
|
||||
?disabled=${this.isCreating}
|
||||
>
|
||||
Cancel
|
||||
</arx-phora-button>
|
||||
</arx-arbor-button>
|
||||
|
||||
<arx-phora-button
|
||||
<arx-arbor-button
|
||||
@click=${this.doCreatePost}
|
||||
?disabled=${this.isCreating}
|
||||
>
|
||||
${this.isCreating ? 'Creating...' : 'Create'}
|
||||
</arx-phora-button>
|
||||
</arx-arbor-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -3,8 +3,8 @@ import { NDKEvent } from '@nostr-dev-kit/ndk';
|
|||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-phora-topic-creator')
|
||||
export class PhoraTopicCreator extends LitElement {
|
||||
@customElement('arx-arbor-topic-creator')
|
||||
export class ArborTopicCreator extends LitElement {
|
||||
@property({ type: String })
|
||||
categoryId = '';
|
||||
|
||||
|
@ -66,10 +66,10 @@ export class PhoraTopicCreator extends LitElement {
|
|||
try {
|
||||
await getSigner();
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 11;
|
||||
event.kind = 892;
|
||||
event.tags = [
|
||||
['subject', this.newTopic],
|
||||
['e', this.categoryId],
|
||||
['name', this.newTopic],
|
||||
['d', `60891:${this.categoryId}`],
|
||||
];
|
||||
event.content = this.topicContent;
|
||||
await event.sign();
|
||||
|
@ -124,19 +124,19 @@ export class PhoraTopicCreator extends LitElement {
|
|||
></textarea>
|
||||
|
||||
<div class="button-group">
|
||||
<arx-phora-button
|
||||
<arx-arbor-button
|
||||
@click=${() => this.dispatchEvent(new CustomEvent('cancel'))}
|
||||
?disabled=${this.isCreating}
|
||||
>
|
||||
Cancel
|
||||
</arx-phora-button>
|
||||
</arx-arbor-button>
|
||||
|
||||
<arx-phora-button
|
||||
<arx-arbor-button
|
||||
@click=${this.doCreateTopic}
|
||||
?disabled=${this.isCreating}
|
||||
>
|
||||
${this.isCreating ? 'Creating...' : 'Create'}
|
||||
</arx-phora-button>
|
||||
</arx-arbor-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
209
src/routes/Arbor/TopicView.ts
Normal file
209
src/routes/Arbor/TopicView.ts
Normal file
|
@ -0,0 +1,209 @@
|
|||
import { getSigner, ndk } from '@/ndk';
|
||||
import type { NDKKind, NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
import '@components/Breadcrumbs';
|
||||
import '@components/Arbor/ForumPost';
|
||||
import '@components/Arbor/Button';
|
||||
|
||||
import { map } from 'lit/directives/map.js';
|
||||
import { when } from 'lit/directives/when.js';
|
||||
|
||||
interface ForumPost {
|
||||
id: string;
|
||||
npub: string;
|
||||
date: Date;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@customElement('arx-arbor-topic-view')
|
||||
export class ArborTopicView extends LitElement {
|
||||
@property({ type: String })
|
||||
topicId = '';
|
||||
|
||||
@state()
|
||||
override title = '';
|
||||
|
||||
@state()
|
||||
private posts: ForumPost[] = [];
|
||||
|
||||
private subscription: NDKSubscription | undefined;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
padding: 1rem;
|
||||
color: #1a1a1a;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
}
|
||||
|
||||
.topic-container {
|
||||
background: #ffffff;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
|
||||
overflow: hidden;
|
||||
margin-top: 1.5rem;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--primary);
|
||||
color: oklch(100% 0 0);
|
||||
padding: 1.5rem 2rem;
|
||||
font-weight: 600;
|
||||
font-size: 1.25rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
background: #f9f9f9;
|
||||
padding: 1.5rem;
|
||||
min-height: 300px;
|
||||
}
|
||||
|
||||
.actions {
|
||||
background: #f9f9f9;
|
||||
padding: 1.25rem 1.5rem;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.06);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
padding: 3rem;
|
||||
text-align: center;
|
||||
color: #666;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.header {
|
||||
padding: 1.25rem 1.5rem;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
:host {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadTopic();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.subscription) {
|
||||
this.subscription.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTopic() {
|
||||
try {
|
||||
await getSigner();
|
||||
const event = await ndk.fetchEvent(this.topicId);
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Could not load topic');
|
||||
}
|
||||
|
||||
this.title = event.tags.find((tag) => tag[0] === 'name')?.[1] || '';
|
||||
|
||||
this.posts = [
|
||||
{
|
||||
id: event.id,
|
||||
npub: event.pubkey,
|
||||
date: new Date((event.created_at || 0) * 1000),
|
||||
content: event.content,
|
||||
},
|
||||
];
|
||||
|
||||
// Subscribe to new posts
|
||||
this.subscription = ndk
|
||||
.subscribe({
|
||||
kinds: [893 as NDKKind],
|
||||
'#E': [this.topicId],
|
||||
})
|
||||
.on('event', (event) => {
|
||||
this.posts = [
|
||||
...this.posts,
|
||||
{
|
||||
id: event.id,
|
||||
npub: event.pubkey,
|
||||
date: new Date((event.created_at || 0) * 1000),
|
||||
content: event.content,
|
||||
},
|
||||
];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load topic:', error);
|
||||
alert('Could not load topic');
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const breadcrumbItems = [{ text: 'Home', href: '/' }, { text: 'Arbor', href: '/arbor' }, { text: this.title }];
|
||||
|
||||
return html`
|
||||
<arx-breadcrumbs .items=${breadcrumbItems}></arx-breadcrumbs>
|
||||
|
||||
<div class="topic-container">
|
||||
<div class="header">
|
||||
<span>${this.title || 'Loading topic...'}</span>
|
||||
</div>
|
||||
|
||||
<div class="posts-container">
|
||||
${when(
|
||||
this.posts.length === 0,
|
||||
() => html`<div class="empty-state">No posts available</div>`,
|
||||
() =>
|
||||
map(
|
||||
this.posts,
|
||||
(post) => html`
|
||||
<arx-forum-post
|
||||
id=${post.id}
|
||||
.topicId=${this.topicId}
|
||||
.npub=${post.npub}
|
||||
.date=${post.date}
|
||||
.content=${post.content}
|
||||
></arx-forum-post>
|
||||
`,
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<arx-arbor-button href="/arbor/new-post/${this.topicId}">
|
||||
New Post
|
||||
</arx-arbor-button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -39,8 +39,8 @@ export class Home extends LitElement {
|
|||
},
|
||||
{
|
||||
id: 3,
|
||||
href: 'phora',
|
||||
name: 'Phora',
|
||||
href: 'arbor',
|
||||
name: 'Arbor',
|
||||
color: '#FF3B30',
|
||||
icon: 'bxs:conversation',
|
||||
},
|
||||
|
|
|
@ -1,122 +0,0 @@
|
|||
import { getSigner, ndk } from '@/ndk';
|
||||
import type { NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import formatDateTime from '@utils/formatDateTime';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
import '@components/Breadcrumbs';
|
||||
import '@components/PhoraForumCategory';
|
||||
import '@components/PhoraForumTopic';
|
||||
import '@components/PhoraButton';
|
||||
|
||||
interface ForumTopic {
|
||||
id: string;
|
||||
title: string;
|
||||
author: string;
|
||||
description: string;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface ForumCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
topics: ForumTopic[];
|
||||
}
|
||||
|
||||
@customElement('arx-phora-home')
|
||||
export class PhoraForum extends LitElement {
|
||||
@state()
|
||||
private categories: ForumCategory[] = [];
|
||||
|
||||
private categoriesQuery: NDKSubscription | undefined;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
`;
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadCategories();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.categoriesQuery) this.categoriesQuery.stop();
|
||||
}
|
||||
|
||||
private async loadCategories() {
|
||||
await getSigner();
|
||||
this.categoriesQuery = ndk
|
||||
.subscribe({
|
||||
kinds: [11],
|
||||
})
|
||||
.on('event', (event) => {
|
||||
const subject = event.tags.find((tag: string[]) => tag[0] === 'subject');
|
||||
const parent = event.tags.find((tag: string[]) => tag[0] === 'e');
|
||||
|
||||
if (!subject) return;
|
||||
|
||||
if (parent) {
|
||||
const categoryIndex = this.categories.findIndex((category) => category.id === parent[1]);
|
||||
if (categoryIndex === -1) return;
|
||||
|
||||
const updatedCategories = [...this.categories];
|
||||
updatedCategories[categoryIndex].topics.push({
|
||||
id: event.id,
|
||||
title: subject[1],
|
||||
author: event.pubkey,
|
||||
created_at: formatDateTime((event.created_at || 0) * 1000),
|
||||
description: event.content,
|
||||
});
|
||||
|
||||
this.categories = updatedCategories;
|
||||
return;
|
||||
}
|
||||
|
||||
this.categories = [
|
||||
...this.categories,
|
||||
{
|
||||
id: event.id,
|
||||
name: subject[1],
|
||||
description: event.content.substring(0, 100),
|
||||
topics: [],
|
||||
},
|
||||
];
|
||||
});
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<arx-breadcrumbs
|
||||
.items=${[{ text: 'Home', href: '/' }, { text: 'Phora' }]}
|
||||
>
|
||||
</arx-breadcrumbs>
|
||||
|
||||
<arx-phora-button href="/phora/new-category">
|
||||
New Category
|
||||
</arx-phora-button>
|
||||
|
||||
${this.categories.map(
|
||||
(category) => html`
|
||||
<arx-phora-forum-category id=${category.id} title=${category.name}>
|
||||
${category.topics.map(
|
||||
(topic) => html`
|
||||
<arx-phora-forum-topic
|
||||
id=${topic.id}
|
||||
title=${topic.title}
|
||||
description=${topic.description}
|
||||
lastPostBy=${topic.author}
|
||||
lastPostTime=${topic.created_at}
|
||||
>
|
||||
</arx-phora-forum-topic>
|
||||
`,
|
||||
)}
|
||||
</arx-phora-forum-category>
|
||||
`,
|
||||
)}
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
import { getSigner, ndk } from '@/ndk';
|
||||
import { NDKEvent } from '@nostr-dev-kit/ndk';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, state } from 'lit/decorators.js';
|
||||
|
||||
@customElement('arx-phora-category-creator')
|
||||
export class PhoraCategoryCreator extends LitElement {
|
||||
@state()
|
||||
private newCategory = '';
|
||||
|
||||
@state()
|
||||
private categoryDescription = '';
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
input,
|
||||
textarea {
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
}
|
||||
`;
|
||||
|
||||
private async doCreateCategory() {
|
||||
if (this.newCategory.length < 3) {
|
||||
alert('Category name must be at least 3 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.categoryDescription.length < 10) {
|
||||
alert('Category description must be at least 10 characters long');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await getSigner();
|
||||
const event = new NDKEvent(ndk);
|
||||
event.kind = 11;
|
||||
event.tags = [['subject', this.newCategory]];
|
||||
event.content = this.categoryDescription;
|
||||
await event.sign();
|
||||
await event.publish();
|
||||
|
||||
this.dispatchEvent(
|
||||
new CustomEvent('category-created', {
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
}),
|
||||
);
|
||||
|
||||
this.newCategory = '';
|
||||
this.categoryDescription = '';
|
||||
} catch (error) {
|
||||
console.error('Failed to create category:', error);
|
||||
alert('Failed to create category');
|
||||
}
|
||||
}
|
||||
|
||||
private handleCategoryInput(e: InputEvent) {
|
||||
this.newCategory = (e.target as HTMLInputElement).value;
|
||||
}
|
||||
|
||||
private handleDescriptionInput(e: InputEvent) {
|
||||
this.categoryDescription = (e.target as HTMLTextAreaElement).value;
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`
|
||||
<input
|
||||
type="text"
|
||||
placeholder="New Category"
|
||||
.value=${this.newCategory}
|
||||
@input=${this.handleCategoryInput}
|
||||
/>
|
||||
|
||||
<textarea
|
||||
placeholder="Category Description"
|
||||
.value=${this.categoryDescription}
|
||||
@input=${this.handleDescriptionInput}
|
||||
></textarea>
|
||||
|
||||
<arx-phora-button @click=${this.doCreateCategory}>
|
||||
Create
|
||||
</arx-phora-button>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,154 +0,0 @@
|
|||
import { getSigner, ndk } from '@/ndk';
|
||||
import type { NDKSubscription } from '@nostr-dev-kit/ndk';
|
||||
import { LitElement, css, html } from 'lit';
|
||||
import { customElement, property, state } from 'lit/decorators.js';
|
||||
|
||||
import '@components/Breadcrumbs';
|
||||
import '@components/ForumPost';
|
||||
import '@components/PhoraButton';
|
||||
|
||||
interface ForumPost {
|
||||
id: string;
|
||||
npub: string;
|
||||
date: Date;
|
||||
content: string;
|
||||
}
|
||||
|
||||
@customElement('arx-phora-topic-view')
|
||||
export class PhoraTopicView extends LitElement {
|
||||
@property({ type: String })
|
||||
topicId = '';
|
||||
|
||||
@state()
|
||||
override title = '';
|
||||
|
||||
@state()
|
||||
private posts: ForumPost[] = [];
|
||||
|
||||
private subscription: NDKSubscription | undefined;
|
||||
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.topic {
|
||||
max-width: 1200px;
|
||||
padding: 1em;
|
||||
background: rgba(255, 255, 255, 0.8);
|
||||
border-radius: 0.5rem;
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: [column-1] auto [column-2] 1fr;
|
||||
}
|
||||
|
||||
.header {
|
||||
background: var(--primary);
|
||||
color: oklch(100% 0 0);
|
||||
padding: 1rem 1.5rem;
|
||||
border-radius: 0.5rem 0.5rem 0 0;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.posts-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.actions {
|
||||
margin-top: 1rem;
|
||||
}
|
||||
`;
|
||||
|
||||
override async connectedCallback() {
|
||||
super.connectedCallback();
|
||||
await this.loadTopic();
|
||||
}
|
||||
|
||||
override disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
if (this.subscription) {
|
||||
this.subscription.stop();
|
||||
}
|
||||
}
|
||||
|
||||
private async loadTopic() {
|
||||
try {
|
||||
await getSigner();
|
||||
const event = await ndk.fetchEvent(this.topicId);
|
||||
|
||||
if (!event) {
|
||||
throw new Error('Could not load topic');
|
||||
}
|
||||
|
||||
this.title = event.tags.find((tag) => tag[0] === 'subject')?.[1] || '';
|
||||
|
||||
this.posts = [
|
||||
{
|
||||
id: event.id,
|
||||
npub: event.pubkey,
|
||||
date: new Date((event.created_at || 0) * 1000),
|
||||
content: event.content,
|
||||
},
|
||||
];
|
||||
|
||||
// Subscribe to new posts
|
||||
this.subscription = ndk
|
||||
.subscribe({
|
||||
kinds: [1111],
|
||||
'#e': [this.topicId],
|
||||
})
|
||||
.on('event', (event) => {
|
||||
this.posts = [
|
||||
...this.posts,
|
||||
{
|
||||
id: event.id,
|
||||
npub: event.pubkey,
|
||||
date: new Date((event.created_at || 0) * 1000),
|
||||
content: event.content,
|
||||
},
|
||||
];
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to load topic:', error);
|
||||
alert('Could not load topic');
|
||||
}
|
||||
}
|
||||
|
||||
override render() {
|
||||
const breadcrumbItems = [{ text: 'Home', href: '/' }, { text: 'Phora', href: '/phora' }, { text: this.title }];
|
||||
|
||||
return html`
|
||||
<arx-breadcrumbs .items=${breadcrumbItems}></arx-breadcrumbs>
|
||||
|
||||
<div class="header">
|
||||
<span>${this.title}</span>
|
||||
</div>
|
||||
|
||||
<div class="posts-container">
|
||||
${this.posts.map(
|
||||
(post) => html`
|
||||
<arx-forum-post
|
||||
class="topic"
|
||||
id=${post.id}
|
||||
.topicId=${this.topicId}
|
||||
.npub=${post.npub}
|
||||
.date=${post.date}
|
||||
.content=${post.content}
|
||||
></arx-forum-post>
|
||||
`,
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div class="actions">
|
||||
<arx-phora-button href="/phora/new-post/${this.topicId}">
|
||||
New Post
|
||||
</arx-phora-button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
|
@ -1,11 +1,10 @@
|
|||
import '@routes/404Page';
|
||||
import '@routes/Home';
|
||||
import '@routes/Profile';
|
||||
import '@routes/Phora/Home';
|
||||
import '@routes/Phora/NewCategory';
|
||||
import '@routes/Phora/NewTopic';
|
||||
import '@routes/Phora/TopicView';
|
||||
import '@routes/Phora/NewPost';
|
||||
import '@routes/Arbor/Home';
|
||||
import '@routes/Arbor/NewTopic';
|
||||
import '@routes/Arbor/TopicView';
|
||||
import '@routes/Arbor/NewPost';
|
||||
import '@components/InitialSetup';
|
||||
|
||||
import { spread } from '@open-wc/lit-helpers';
|
||||
|
@ -40,29 +39,24 @@ export default class EveRouter extends LitElement {
|
|||
component: literal`arx-profile-route`,
|
||||
},
|
||||
{
|
||||
pattern: 'phora',
|
||||
pattern: 'arbor',
|
||||
params: {},
|
||||
component: literal`arx-phora-home`,
|
||||
component: literal`arx-arbor-home`,
|
||||
},
|
||||
{
|
||||
pattern: 'phora/new-category',
|
||||
pattern: 'arbor/new-topic/:categoryId',
|
||||
params: {},
|
||||
component: literal`arx-phora-category-creator`,
|
||||
component: literal`arx-arbor-topic-creator`,
|
||||
},
|
||||
{
|
||||
pattern: 'phora/new-topic/:categoryId',
|
||||
pattern: 'arbor/topics/:topicId',
|
||||
params: {},
|
||||
component: literal`arx-phora-topic-creator`,
|
||||
component: literal`arx-arbor-topic-view`,
|
||||
},
|
||||
{
|
||||
pattern: 'phora/topics/:topicId',
|
||||
pattern: 'arbor/new-post/:topicId',
|
||||
params: {},
|
||||
component: literal`arx-phora-topic-view`,
|
||||
},
|
||||
{
|
||||
pattern: 'phora/new-post/:topicId',
|
||||
params: {},
|
||||
component: literal`arx-phora-post-creator`,
|
||||
component: literal`arx-arbor-post-creator`,
|
||||
},
|
||||
{
|
||||
pattern: '404',
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue