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
<p>
Aren't you tired of distracting social media platforms?
<p>Aren't you tired of distracting social media platforms?
So many buttons and advertising makes a simple task of
reading and composing notes to your community very draining. You can use
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>
<p>
I'm using <a href="https://prosemirror.net/">ProseMirror</a>,
<p>I'm using <a href="https://prosemirror.net/">ProseMirror</a>,
to build a WYSIWYM style rich text editor for visitors
to compose notes with. I'm taking inspiration from tumblr
and medium's text editors to build something minimal and
intuitive that will run easily via browsers.
</p>
intuitive that will run easily via browsers. </p>
<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".
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

12
package-lock.json generated
View file

@ -19,6 +19,7 @@
"express": "^4.21.1",
"lazy.js": "^0.5.1",
"lit": "^3.2.1",
"marked": "^15.0.6",
"nostr-tools": "^2.10.1",
"prosemirror-markdown": "^1.13.1",
"prosemirror-menu": "^1.2.4",
@ -4460,6 +4461,17 @@
"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": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz",

View file

@ -25,6 +25,7 @@
"express": "^4.21.1",
"lazy.js": "^0.5.1",
"lit": "^3.2.1",
"marked": "^15.0.6",
"nostr-tools": "^2.10.1",
"prosemirror-markdown": "^1.13.1",
"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",
"scope": "/",
"id": "https://miguelalmodo.com/",
"scope": "/dist/",
"name": "fostr",
"display": "standalone",
"start_url": "/",
"short_name": "micro app",
"start_url": "/dist/",
"short_name": "miggymofongo",
"theme_color": "#E1477E",
"description": "This is a miggymofongo project",
"orientation": "any",
@ -17,12 +17,12 @@
},
"icons": [
{
"src": "assets/icons/512x512.png",
"src": "assets/icons/gold_crown.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "assets/icons/192x192.png",
"src": "assets/icons/144x144.png",
"sizes": "192x192",
"type": "image/png"
},
@ -32,10 +32,11 @@
"type": "image/png"
},
{
"src": "assets/icons/24x24.png",
"src": "assets/icons/20x20.png",
"sizes": "24x24",
"type": "image/png"
}
],
"screenshots": [
{
@ -67,7 +68,7 @@
"tag": "starterWidget",
"ms_ac_template": "widget/ac.json",
"data": "widget/data.json",
"description": "A simple widget example from pwa-starter.",
"description": "Coming soon",
"screenshots": [
{
"src": "assets/screenshots/widget-screen.png",

View file

@ -23,8 +23,8 @@ export class AppHeader extends LitElement {
align-items: center;
background: var(--app-color-primary);
color: white;
padding: 12px;
padding-top: 4px;
padding: 40px;
padding-top: 40px;
position: fixed;
left: env(titlebar-area-x, 0);
@ -61,6 +61,10 @@ export class AppHeader extends LitElement {
color: initial;
}
}
#signin {
padding-right: 80px;
}
`;
/** 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.
@ -163,14 +167,15 @@ export class AppHeader extends LitElement {
${this.enableBack ? html`<sl-button size="small" href="${resolveRouterPath()}">
Back
</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 id="signin">
<sl-button variant="primary" @click="${this.isSignedIn ? this.signOut : this.signInWithNostr}">
${this.isSignedIn ? 'Sign out' : 'Sign in'}
</sl-button>
</sl-button></div>
</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,
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
@ -62,13 +48,10 @@ Profile Picture Container
}
.profile-picture-container p {
margin: 0;
}
.profile-pic {
grid-area: 1/1;
margin-top: 5px;
margin-top: 30px;
margin-left: 100px;
margin-bottom: 5px;
@ -129,7 +112,7 @@ Profile Picture Container
}
handlesSignOut() {
this.profilePic = '';
this.profilePic = '/dist/assets/img/default_pfp.png';
this.bio = '';
this.nostrAddy = 'Guest';
this.isSignedIn = false;
@ -139,17 +122,16 @@ Profile Picture Container
async fetchAndDisplayProfile(pubkey: string) {
this.nostrAddy = pubkey;
this.bio = 'Loading profile info...';
this.profilePic = '/assets/img/loading_pfp.png';
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 sub = relay.subscribe([{ kinds: [0], authors: [pubkey] }], {
onevent: (event) => {
const profileData = JSON.parse(event.content);
this.nostrAddy = profileData.nip05 || 'No address 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();
},
oneose: () => sub.close(),
@ -165,7 +147,7 @@ Profile Picture Container
// Set initial values for guest view
this.nostrAddy = '';
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;
}
@ -175,7 +157,7 @@ Profile Picture Container
(navigator as any).share({
title: 'A MiggyMofongo Project',
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>
</div>
<p>
You can upgrade your website into
a micro blog client with a social protocol to do things
like browse a feed, compose a note, or post to your network.
You can upgrade your personal website with a social
protocol to do things like browse a feed, compose
a note, or post to your network.
</p>
<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>
@ -215,7 +196,7 @@ Profile Picture Container
${'share' in navigator
? html`<sl-button slot="footer" variant="default" @click="${this.share}">
<sl-icon slot="prefix" name="share"></sl-icon>
Share this personal website with a friend!
Share with a homie!
</sl-button>`
: null}
</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 '@shoelace-style/shoelace/dist/components/card/card.js';
@ -160,7 +160,16 @@ export class AppWrite extends LitElement {
static 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() {
@ -209,7 +218,7 @@ export class AppWrite extends LitElement {
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('paragraph', null, [customSchema.text('"WEPA"')]),
@ -220,8 +229,9 @@ export class AppWrite extends LitElement {
]),
customSchema.node('horizontal_rule', null),
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('Presiona Ctrl + g para marcar la selección con la marca "shouting" ')])
customSchema.node('paragraph', null, [customSchema.text('Presione Shift + Space para colocar una estrella')]),
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() {
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>
`;

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`
:host .ProseMirror {
background: black;
color: white;
background-clip: padding-box;
padding: 5px 0;
@ -31,36 +31,75 @@ export const editorStyles = css`
--markdown-editor-typography-letter-spacing,
var(--mdc-typography-subtitle1-letter-spacing, 0.009375em)
);
}
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror {
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 {
color: var(--markdown-editor-typography-anchor-color, -webkit-link);
text-decoration: var(--markdown-editor-typography-anchor-text-decoration);
}
.ProseMirror pre {
white-space: pre-wrap;
}
.ProseMirror-focused .ProseMirror-gapcursor {
display: block;
}
shouting {
all: unset; /* Remove inherited or conflicting styles */
font-weight: bold;
text-transform: uppercase;
color: red; }
.ProseMirror li {
position: relative;
}
.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;
}
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 { property, customElement } from 'lit/decorators.js';
import { unsafeHTML } from 'lit/directives/unsafe-html.js'
import { Relay } from 'nostr-tools';
import {marked} from 'marked';
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
@ -23,71 +25,37 @@ note = ''; // store notes
styles,
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 {
margin: auto;
margin-bottom: 5px;
color: black;
margin-bottom: 50px;
}
.comment-wall th {
width: 158px;
padding: 3px;
vertical-align: top;
}
.comment-wall td {
vertical-align: top;
width: 269px;
width: 600px;
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 {
text-align: right;
margin-right: 10px;
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() {
return html`
<app-header ?enableBack="${true}"></app-header>
<main> <div id="welcomeBar">
<sl-card id="WelcomeCard">
<section>
<header class="main-section-header">
<h2 class="main-section-h2">Recent Notes from ${this.relayName}</h2>
</header>
<table class="comment-wall">
${this.notes.map(note => {
// extract URL from note content
const urlMatch = note.content.match(/https?:\/\/[^\s]+/);
const textContent = note.content.replace(urlMatch?.[0] || '', '').trim();
// Check for yt links and extract video ID
const youtubeRegex = /(?:https?:\/\/(?:www\.)?youtube\.com\/watch\?v=|https?:\/\/youtu\.be\/)([a-zA-Z0-9_-]{11})/;
const youtubeMatch = urlMatch?.[0].match(youtubeRegex);
const youtubeVideoId = youtubeMatch ? youtubeMatch[1] : null;
// build yt thumbnail URL if applicable
const thumbnailUrl = youtubeVideoId
? `https://img.youtube.com/vi/${youtubeVideoId}/maxresdefault.jpg`
: null;
//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>
<main>
<div id="welcomeBar">
<sl-card id="WelcomeCard">
<section>
<header class="">
<h2 class="">
Recent Notes from ${this.relayName}
</h2>
</header>
<table class="comment-wall">
${this.notes.map(
(note) => html`
<tr>
<td>
<h3>${note.date}</h3>
</td>
<td class="note-content">
${unsafeHTML(marked(note.content) as string)}
</td>
</tr>
`
)}
</table>
</section>
</sl-card>
</div>
</main>
`;
}
}

View file

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

View file

@ -6,6 +6,7 @@ export const styles = css`
main {
margin-top: 100px;
padding: 12px;
background-color: ;
}
@ -40,10 +41,29 @@ export const styles = css`
}
}
@media (horizontal-viewport-segments: 2) {
@media (horizontal-viewport-segments: 2) {
#welcomeBar {
flex-direction: row;
align-items: flex-start;
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%;">`
: ''} -->
*/