added marked and unsafe-html to render markdown from

long form notes and adjusted note wall.
looking at tumblr for inspiration here...
This commit is contained in:
miggymofongo 2025-01-08 23:59:05 -04:00
parent 8abd046aff
commit 7c0e1bd008
23 changed files with 200 additions and 317 deletions

View file

@ -1,7 +1,6 @@
# Personal Microclient # Personal Microclient
<p> <p>Aren't you tired of distracting social media platforms?
Aren't you tired of distracting social media platforms?
So many buttons and advertising makes a simple task of So many buttons and advertising makes a simple task of
reading and composing notes to your community very draining. You can use reading and composing notes to your community very draining. You can use
a next generation social media protocol to transform a next generation social media protocol to transform
@ -9,13 +8,11 @@ your personal website into a micro social media client
that will clear up space on your phone for media. </p> that will clear up space on your phone for media. </p>
<p> <p>I'm using <a href="https://prosemirror.net/">ProseMirror</a>,
I'm using <a href="https://prosemirror.net/">ProseMirror</a>,
to build a WYSIWYM style rich text editor for visitors to build a WYSIWYM style rich text editor for visitors
to compose notes with. I'm taking inspiration from tumblr to compose notes with. I'm taking inspiration from tumblr
and medium's text editors to build something minimal and and medium's text editors to build something minimal and
intuitive that will run easily via browsers. intuitive that will run easily via browsers. </p>
</p>
<p>I scaffolded the project with PWA Builder and am using <p>I scaffolded the project with PWA Builder and am using
@ -29,6 +26,16 @@ from a relay. </p>
<p>If you're on any chrome-based, firefox or safari browser try visiting the webpage then tapping on the arrow pointing up in the bottom toolbar. Scroll down a bit to tap on "Add to Home Screen". <p>If you're on any chrome-based, firefox or safari browser try visiting the webpage then tapping on the arrow pointing up in the bottom toolbar. Scroll down a bit to tap on "Add to Home Screen".
If you're on a chromium-based browser you should be able to do the same.</p> If you're on a chromium-based browser you should be able to do the same.</p>
<p>Potential Use cases
Put public library record digitized in a nostr client feed for their public records
blog feed and reflect activity
for pitch to group, have a side by side example of blog written in markdown, microsoft word, and rich text.
</p>
# TODO # TODO

12
package-lock.json generated
View file

@ -19,6 +19,7 @@
"express": "^4.21.1", "express": "^4.21.1",
"lazy.js": "^0.5.1", "lazy.js": "^0.5.1",
"lit": "^3.2.1", "lit": "^3.2.1",
"marked": "^15.0.6",
"nostr-tools": "^2.10.1", "nostr-tools": "^2.10.1",
"prosemirror-markdown": "^1.13.1", "prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4", "prosemirror-menu": "^1.2.4",
@ -4460,6 +4461,17 @@
"markdown-it": "bin/markdown-it.mjs" "markdown-it": "bin/markdown-it.mjs"
} }
}, },
"node_modules/marked": {
"version": "15.0.6",
"resolved": "https://registry.npmjs.org/marked/-/marked-15.0.6.tgz",
"integrity": "sha512-Y07CUOE+HQXbVDCGl3LXggqJDbXDP2pArc2C1N1RRMN0ONiShoSsIInMd5Gsxupe7fKLpgimTV+HOJ9r7bA+pg==",
"bin": {
"marked": "bin/marked.js"
},
"engines": {
"node": ">= 18"
}
},
"node_modules/mdurl": { "node_modules/mdurl": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",

View file

@ -25,6 +25,7 @@
"express": "^4.21.1", "express": "^4.21.1",
"lazy.js": "^0.5.1", "lazy.js": "^0.5.1",
"lit": "^3.2.1", "lit": "^3.2.1",
"marked": "^15.0.6",
"nostr-tools": "^2.10.1", "nostr-tools": "^2.10.1",
"prosemirror-markdown": "^1.13.1", "prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4", "prosemirror-menu": "^1.2.4",

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 66 KiB

View file

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

BIN
public/assets/img/nostr-icon.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

View file

@ -1,10 +1,10 @@
{ {
"id": "https://miguelalmodo.com/dist2", "id": "https://miguelalmodo.com/",
"scope": "/", "scope": "/dist/",
"name": "fostr", "name": "fostr",
"display": "standalone", "display": "standalone",
"start_url": "/", "start_url": "/dist/",
"short_name": "micro app", "short_name": "miggymofongo",
"theme_color": "#E1477E", "theme_color": "#E1477E",
"description": "This is a miggymofongo project", "description": "This is a miggymofongo project",
"orientation": "any", "orientation": "any",
@ -17,12 +17,12 @@
}, },
"icons": [ "icons": [
{ {
"src": "assets/icons/512x512.png", "src": "assets/icons/gold_crown.png",
"sizes": "512x512", "sizes": "512x512",
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "assets/icons/192x192.png", "src": "assets/icons/144x144.png",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}, },
@ -32,10 +32,11 @@
"type": "image/png" "type": "image/png"
}, },
{ {
"src": "assets/icons/24x24.png", "src": "assets/icons/20x20.png",
"sizes": "24x24", "sizes": "24x24",
"type": "image/png" "type": "image/png"
} }
], ],
"screenshots": [ "screenshots": [
{ {
@ -67,7 +68,7 @@
"tag": "starterWidget", "tag": "starterWidget",
"ms_ac_template": "widget/ac.json", "ms_ac_template": "widget/ac.json",
"data": "widget/data.json", "data": "widget/data.json",
"description": "A simple widget example from pwa-starter.", "description": "Coming soon",
"screenshots": [ "screenshots": [
{ {
"src": "assets/screenshots/widget-screen.png", "src": "assets/screenshots/widget-screen.png",

View file

@ -23,8 +23,8 @@ export class AppHeader extends LitElement {
align-items: center; align-items: center;
background: var(--app-color-primary); background: var(--app-color-primary);
color: white; color: white;
padding: 12px; padding: 40px;
padding-top: 4px; padding-top: 40px;
position: fixed; position: fixed;
left: env(titlebar-area-x, 0); left: env(titlebar-area-x, 0);
@ -61,6 +61,10 @@ export class AppHeader extends LitElement {
color: initial; color: initial;
} }
} }
#signin {
padding-right: 80px;
}
`; `;
/** the connected callback method one of the lifecycle methods that runs once the element is added /** the connected callback method one of the lifecycle methods that runs once the element is added
* to the dom. there is no disconnectedCallback because the header is never removed from the DOM. * to the dom. there is no disconnectedCallback because the header is never removed from the DOM.
@ -163,14 +167,15 @@ export class AppHeader extends LitElement {
${this.enableBack ? html`<sl-button size="small" href="${resolveRouterPath()}"> ${this.enableBack ? html`<sl-button size="small" href="${resolveRouterPath()}">
Back Back
</sl-button>` : null} </sl-button>` : null}
<img src="/dist/assets/img/nostr-icon.png" alt="Nostr Icon" class="nostr-icon" style="width: 50px"/>
<h1>${this.title}</h1> <h2>${this.title}</h2>
</div> </div>
<div id="signin">
<sl-button variant="primary" @click="${this.isSignedIn ? this.signOut : this.signInWithNostr}"> <sl-button variant="primary" @click="${this.isSignedIn ? this.signOut : this.signInWithNostr}">
${this.isSignedIn ? 'Sign out' : 'Sign in'} ${this.isSignedIn ? 'Sign out' : 'Sign in'}
</sl-button> </sl-button></div>
</header> </header>
`; `;
} }

View file

@ -1,37 +0,0 @@
// menu-plugin.ts
import { MenuItem } from "prosemirror-menu";
import { menuBar } from "prosemirror-menu";
import { toggleMark } from "prosemirror-commands";
import { Plugin, PluginKey } from "prosemirror-state";
const menuPluginKey = new PluginKey('menuPlugin')
// Modify the menuBar function to accept customSchema as a parameter
export function createMenuPlugin(customSchema: any) {
const boldButton = new MenuItem({
title: "Bold",
run: toggleMark(customSchema.marks.bold),
active: (state) => state.selection.$head.marks().some(mark => mark.type === customSchema.marks.bold),
});
const italicButton = new MenuItem({
title: "Italic",
run: toggleMark(customSchema.marks.italic),
active: (state) => state.selection.$head.marks().some(mark => mark.type === customSchema.marks.italic),
});
const menuContent = [
[boldButton, italicButton] // Array of buttons or other menu items
];
menuBar({
content: menuContent,
floating: true,
});
console.log('Creating menu plugin');
return new Plugin({
key: menuPluginKey
})
}

View file

@ -34,20 +34,6 @@ export class AppHome extends LitElement {
styles, styles,
css` css`
@media (horizontal-viewport-segments: 2) {
#welcomeBar {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
#welcomeCard {
margin-right: 64px;
}
}
/* /*
======================================== ========================================
Profile Picture Container Profile Picture Container
@ -62,13 +48,10 @@ Profile Picture Container
} }
.profile-picture-container p {
margin: 0;
}
.profile-pic { .profile-pic {
grid-area: 1/1; grid-area: 1/1;
margin-top: 5px; margin-top: 30px;
margin-left: 100px; margin-left: 100px;
margin-bottom: 5px; margin-bottom: 5px;
@ -129,7 +112,7 @@ Profile Picture Container
} }
handlesSignOut() { handlesSignOut() {
this.profilePic = ''; this.profilePic = '/dist/assets/img/default_pfp.png';
this.bio = ''; this.bio = '';
this.nostrAddy = 'Guest'; this.nostrAddy = 'Guest';
this.isSignedIn = false; this.isSignedIn = false;
@ -139,17 +122,16 @@ Profile Picture Container
async fetchAndDisplayProfile(pubkey: string) { async fetchAndDisplayProfile(pubkey: string) {
this.nostrAddy = pubkey; this.nostrAddy = pubkey;
this.bio = 'Loading profile info...'; this.bio = 'Loading profile info...';
this.profilePic = '/assets/img/loading_pfp.png';
try { try {
// Fetch profile metadata from a relay // try to fetch profile metadata from a relay
const relay = await Relay.connect('wss://notes.miguelalmodo.com'); // Example URL const relay = await Relay.connect('wss://notes.miguelalmodo.com'); // Example URL
const sub = relay.subscribe([{ kinds: [0], authors: [pubkey] }], { const sub = relay.subscribe([{ kinds: [0], authors: [pubkey] }], {
onevent: (event) => { onevent: (event) => {
const profileData = JSON.parse(event.content); const profileData = JSON.parse(event.content);
this.nostrAddy = profileData.nip05 || 'No address available'; this.nostrAddy = profileData.nip05 || 'No address available';
this.bio = profileData.about || 'No bio available'; this.bio = profileData.about || 'No bio available';
this.profilePic = profileData.picture || '/assets/img/default_pfp.png'; this.profilePic = profileData.picture || '/dist/assets/img/default_pfp.png';
this.requestUpdate(); this.requestUpdate();
}, },
oneose: () => sub.close(), oneose: () => sub.close(),
@ -165,7 +147,7 @@ Profile Picture Container
// Set initial values for guest view // Set initial values for guest view
this.nostrAddy = ''; this.nostrAddy = '';
this.bio = 'Welcome, guest! Please sign in with a browser extension to view your profile.'; this.bio = 'Welcome, guest! Please sign in with a browser extension to view your profile.';
this.profilePic = '/assets/img/default_pfp.png'; // Could be a placeholder image for guests this.profilePic = '/dist/assets/img/default_pfp.png';
this.isSignedIn = false; this.isSignedIn = false;
} }
@ -175,7 +157,7 @@ Profile Picture Container
(navigator as any).share({ (navigator as any).share({
title: 'A MiggyMofongo Project', title: 'A MiggyMofongo Project',
text: 'This is a personal progressive social web app', text: 'This is a personal progressive social web app',
url: 'https://miguelalmodo.com/dist2', url: 'https://miguelalmodo.com/dist',
}); });
} }
} }
@ -194,15 +176,14 @@ Profile Picture Container
<h2>Welcome to ${this.nostrAddy || 'Guest'}'s Profile</h2> <h2>Welcome to ${this.nostrAddy || 'Guest'}'s Profile</h2>
</div> </div>
<p> <p>
You can upgrade your website into You can upgrade your personal website with a social
a micro blog client with a social protocol to do things protocol to do things like browse a feed, compose
like browse a feed, compose a note, or post to your network. a note, or post to your network.
</p> </p>
<div class="profile-picture-container"> <div class="profile-picture-container">
<img class="profile-pic" src="${this.profilePic || '/assets/img/default_pfp.png'}" alt="Profile Picture" width="200px" height="200px"> <img class="profile-pic" src="${this.profilePic || '/dist/assets/img/default_pfp.png'}" alt="Profile Picture" width="200px" height="200px">
<p class="personal-msg"><b>${this.bio || 'Welcome, guest! Please sign in to view your profile.'}</b></p> <p class="personal-msg"><b>${this.bio || 'Welcome, guest! Please sign in to view your profile.'}</b></p>
@ -215,7 +196,7 @@ Profile Picture Container
${'share' in navigator ${'share' in navigator
? html`<sl-button slot="footer" variant="default" @click="${this.share}"> ? html`<sl-button slot="footer" variant="default" @click="${this.share}">
<sl-icon slot="prefix" name="share"></sl-icon> <sl-icon slot="prefix" name="share"></sl-icon>
Share this personal website with a friend! Share with a homie!
</sl-button>` </sl-button>`
: null} : null}
</sl-card> </sl-card>

View file

@ -1,4 +1,4 @@
import { LitElement, html } from 'lit'; import { LitElement, html, css } from 'lit';
import { customElement, query } from 'lit/decorators.js'; import { customElement, query } from 'lit/decorators.js';
import '@shoelace-style/shoelace/dist/components/card/card.js'; import '@shoelace-style/shoelace/dist/components/card/card.js';
@ -160,7 +160,16 @@ export class AppWrite extends LitElement {
static styles = [ static styles = [
styles, styles,
editorStyles editorStyles,
css`:root {
--main-line-color: hsl(234, 62%, 86%);
--side-line-color: hsl(350, 100%, 91%);
--paper-color: hsl(0, 15%, 95%);
--ink-color: hsl(0, 0%, 12%);
--line-thickness: 3px;
--top-space: 4lh;
}`
]; ];
constructor() { constructor() {
@ -209,7 +218,7 @@ export class AppWrite extends LitElement {
let doc = customSchema.node('doc', null, [ let doc = customSchema.node('doc', null, [
customSchema.node('heading', null, [customSchema.text('Título')]), customSchema.node('heading', null, [customSchema.text('Título h1')]),
customSchema.node('blockquote', null, [ customSchema.node('blockquote', null, [
customSchema.node('paragraph', null, [customSchema.text('"WEPA"')]), customSchema.node('paragraph', null, [customSchema.text('"WEPA"')]),
@ -220,8 +229,9 @@ export class AppWrite extends LitElement {
]), ]),
customSchema.node('horizontal_rule', null), customSchema.node('horizontal_rule', null),
customSchema.node('paragraph', null, [customSchema.text('Escribe mas aquí...')]), customSchema.node('paragraph', null, [customSchema.text('Escribe mas aquí...')]),
customSchema.node('paragraph', null, [customSchema.text('Presiona Shift + Space para colocar una estrella')]), customSchema.node('paragraph', null, [customSchema.text('Presione Shift + Space para colocar una estrella')]),
customSchema.node('paragraph', null, [customSchema.text('Presiona Ctrl + g para marcar la selección con la marca "shouting" ')]) customSchema.node('paragraph', null, [customSchema.text('Presione Ctrl + g para marcar la selección con la marca "shouting" ')]),
customSchema.node('heading', { level: 2 }, [customSchema.text('Título h2')]),
]) ])
@ -271,10 +281,12 @@ export class AppWrite extends LitElement {
protected render() { protected render() {
return html` return html`
<main><app-header ?enableBack="${true}"></app-header> <app-header ?enableBack="${true}"></app-header>
<main>
<div class="ProseMirror"> Try selecting some text below to test out a minimal rich text editor.
<div class="ProseMirror"></div> </div>
</main> </main>
`; `;

View file

@ -1,101 +0,0 @@
import { LitElement, PropertyValues, css, html } from "lit";
import { styles } from "../../styles/shared-styles";
export class MenuBar extends LitElement {
static properties = {
items: { type: Array }, // Array of menu items
editorView: { type: Object }, // Reference to the editor view
};
constructor() {
super();
this.items = [];
this.editorView = null;
}
static stylse = [styles, css `
.menubar {
display: flex;
gap: 10px;
padding: 5px;
background-color: #f9f9f9;
border-bottom: 1px solid #ddd;
}
.menubar button {
padding: 5px 10px;
border: none;
background-color: #fff;
cursor: pointer;
border-radius: 3px;
font-size: 14px;
}
.menubar button:hover {
background-color: #eee;
}
.menubar button:disabled {
background-color: #ccc;
cursor: not-allowed;
} `, ]
;
async _isItemActive(item) {
if (this.editorView && typeof item.command === 'function') {
return item.command(this.editorView.state, null, this.editorView);
}
return false;
}
async _onItemClick(event, item) {
event.preventDefault();
if (this.editorView) {
this.editorView.focus();
item.command(this.editorView.state, this.editorView.dispatch, this.editorView);
}
}
async _updateMenu() {
if (this.items && this.editorView) {
this.items.forEach((item) => {
const isActive = this._isItemActive(item);
item.dom && (item.dom.style.display = isActive ? '' : 'none');
});
}
}
connectedCallback(): void {
super.connectedCallback();
console.log("editor menu component added");
this._updateMenu();
}
disconnectedCallback(): void {
super.disconnectedCallback();
this.items.forEach((item) => {
if (item.dom) {
item.dom.remove();
}
});
}
render() {
return html`
<app-header></app-header>
<main>
</main>
`
}
}
customElements.define('menu-bar', MenuBar)

View file

@ -2,8 +2,8 @@ import { css } from "lit"
export const editorStyles = css` export const editorStyles = css`
:host .ProseMirror { :host .ProseMirror {
background: black;
color: white; color: white;
background-clip: padding-box; background-clip: padding-box;
padding: 5px 0; padding: 5px 0;
@ -31,36 +31,75 @@ export const editorStyles = css`
--markdown-editor-typography-letter-spacing, --markdown-editor-typography-letter-spacing,
var(--mdc-typography-subtitle1-letter-spacing, 0.009375em) var(--mdc-typography-subtitle1-letter-spacing, 0.009375em)
); );
} }
.ProseMirror pre { .ProseMirror {
white-space: pre-wrap; word-wrap: break-word;
} white-space: pre-wrap;
white-space: break-spaces;
-webkit-font-variant-ligatures: none;
font-variant-ligatures: none;
font-feature-settings: "liga" 0; /* the above doesn't seem to work in Edge */
}
.ProseMirror a { .ProseMirror pre {
color: var(--markdown-editor-typography-anchor-color, -webkit-link); white-space: pre-wrap;
text-decoration: var(--markdown-editor-typography-anchor-text-decoration); }
}
.ProseMirror-focused .ProseMirror-gapcursor { .ProseMirror li {
display: block; position: relative;
} }
shouting {
all: unset; /* Remove inherited or conflicting styles */
font-weight: bold;
text-transform: uppercase;
color: red; }
.boring { .ProseMirror-hideselection *::selection { background: transparent; }
.ProseMirror-hideselection *::-moz-selection { background: transparent; }
.ProseMirror-hideselection { caret-color: transparent; }
/* See https://github.com/ProseMirror/prosemirror/issues/1421#issuecomment-1759320191 */
.ProseMirror [draggable][contenteditable=false] { user-select: text }
.ProseMirror-selectednode {
outline: 2px solid #8cf;
}
/* Make sure li selections wrap around markers */
li.ProseMirror-selectednode {
outline: none;
}
li.ProseMirror-selectednode:after {
content: "";
position: absolute;
left: -32px;
right: -2px; top: -2px; bottom: -2px;
border: 2px solid #8cf;
pointer-events: none;
}
/* Protect against generic img rules */
img.ProseMirror-separator {
display: inline !important;
border: none !important;
margin: 0 !important;
}
shouting {
all: unset; /* Remove inherited or conflicting styles */
font-weight: bold;
text-transform: uppercase;
color: red; }
.boring {
background: grey; background: grey;
} }
blockquote {
font-style: italic;
border-left: 2px solid gray;
padding-left: 10px;
color: darkgray;
blockquote {
font-style: italic;
border-left: 2px solid gray;
padding-left: 10px;
color: darkgray;
}
` `

View file

@ -1,6 +1,8 @@
import { LitElement, css, html } from 'lit'; import { LitElement, css, html } from 'lit';
import { property, customElement } from 'lit/decorators.js'; import { property, customElement } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { Relay } from 'nostr-tools'; import { Relay } from 'nostr-tools';
import {marked} from 'marked';
import '@shoelace-style/shoelace/dist/components/card/card.js'; import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/button/button.js'; import '@shoelace-style/shoelace/dist/components/button/button.js';
@ -23,71 +25,37 @@ note = ''; // store notes
styles, styles,
css` css`
.comment-wall .main-section-header {
margin-bottom: 3px;
}
.comment-wall .main-section-h2 {
margin-bottom: 0;
}
#comment-counter {
margin-top: 0;
margin-left: 15px;
margin-bottom: 3px;
}
.comment-wall table { .comment-wall table {
margin: auto; margin: auto;
margin-bottom: 5px; margin-bottom: 50px;
color: black;
} }
.comment-wall th { .comment-wall th {
width: 158px;
padding: 3px;
vertical-align: top; vertical-align: top;
} }
.comment-wall td { .comment-wall td {
vertical-align: top; vertical-align: top;
width: 269px; width: 600px;
padding: 3px; padding: 3px;
} }
.comment-wall figcaption,
.comment-wall figure {
margin: 0;
}
.comment-wall figcaption {
margin-bottom: 1em;
}
.comment-wall figure {
margin-bottom: 49.33px;
}
.comment-wall h3 {
font-size: 10pt;
margin: 0;
margin-bottom: 3em;
}
.comment-wall p {
font-weight: normal;
text-align: center;
margin: 0;
}
#add-comment { #add-comment {
text-align: right; text-align: right;
margin-right: 10px; margin-right: 10px;
margin-bottom: 5px; margin-bottom: 5px;
} }
.note-content {
text-align: left;
white-space: pre-wrap;
}
.note-content img {
max-width: 100%;
height: auto;
}
`]; `];
@ -184,58 +152,33 @@ async displayLongNotes() {
render() { render() {
return html` return html`
<app-header ?enableBack="${true}"></app-header> <app-header ?enableBack="${true}"></app-header>
<main> <div id="welcomeBar"> <main>
<sl-card id="WelcomeCard"> <div id="welcomeBar">
<section> <sl-card id="WelcomeCard">
<section>
<header class="main-section-header"> <header class="">
<h2 class="main-section-h2">Recent Notes from ${this.relayName}</h2> <h2 class="">
</header> Recent Notes from ${this.relayName}
</h2>
</header>
<table class="comment-wall">
${this.notes.map(
<table class="comment-wall"> (note) => html`
${this.notes.map(note => { <tr>
// extract URL from note content <td>
const urlMatch = note.content.match(/https?:\/\/[^\s]+/); <h3>${note.date}</h3>
const textContent = note.content.replace(urlMatch?.[0] || '', '').trim(); </td>
<td class="note-content">
${unsafeHTML(marked(note.content) as string)}
// Check for yt links and extract video ID </td>
const youtubeRegex = /(?:https?:\/\/(?:www\.)?youtube\.com\/watch\?v=|https?:\/\/youtu\.be\/)([a-zA-Z0-9_-]{11})/; </tr>
const youtubeMatch = urlMatch?.[0].match(youtubeRegex); `
const youtubeVideoId = youtubeMatch ? youtubeMatch[1] : null; )}
</table>
// build yt thumbnail URL if applicable </section>
const thumbnailUrl = youtubeVideoId </sl-card>
? `https://img.youtube.com/vi/${youtubeVideoId}/maxresdefault.jpg` </div>
: null; </main>
//check for image links
const imgRegex = /(https?:\/\/[^\s]+\.(?:jpg|jpeg|png|gif))/i;
const imgMatch = urlMatch?.find(url => imgRegex.test(url));
return html`
<tr>
<td><h3>${note.date}</h3></td>
<td>
<p>${textContent}</p>
${thumbnailUrl
? html`<img src="${thumbnailUrl}" alt="YouTube thumbnail" style="max-width: 30%; height: 30%;">`
: imgMatch
? html`<img src="${imgMatch}" alt="Note image" style="max-width: 30%; height: 30%;">`
: ''}
</td>
</tr>
`;
})}
</table>
</section></main>
</sl-card>
`; `;
} }
} }

View file

@ -34,7 +34,7 @@ export const router = new Router({
}, },
{ {
path: resolveRouterPath('note-wall'), path: resolveRouterPath('note-wall'),
title: 'Note Wall', title: 'Feed',
plugins: [ plugins: [
lazy(() => import('./pages/note-wall.js')), lazy(() => import('./pages/note-wall.js')),
], ],

View file

@ -6,6 +6,7 @@ export const styles = css`
main { main {
margin-top: 100px; margin-top: 100px;
padding: 12px; padding: 12px;
background-color: ;
} }
@ -40,10 +41,29 @@ export const styles = css`
} }
} }
@media (horizontal-viewport-segments: 2) { @media (horizontal-viewport-segments: 2) {
#welcomeBar { #welcomeBar {
flex-direction: row; flex-direction: row;
align-items: flex-start; align-items: flex-start;
justify-content: space-between; justify-content: space-between;
}} }
`
#welcomeCard {
margin-right: 64px;
}
}
`
/**
* <!-- <p>${textContent}</p>
${thumbnailUrl
? html`<img src="${thumbnailUrl}" alt="YouTube thumbnail" style="max-width: 30%; height: 30%;">`
: imgMatch
? html`<img src="${imgMatch}" alt="Note image" style="max-width: 30%; height: 30%;">`
: ''} -->
*/