initial commit okayyy

This commit is contained in:
miggymofongo 2024-11-13 00:55:50 -04:00
commit a59b6a5cfd
50 changed files with 6690 additions and 0 deletions

8
.gitignore vendored Normal file
View file

@ -0,0 +1,8 @@
.DS_Store
node_modules
dist
dev-dist
build
types
.idea
.github

8
.vscode/extensions.json vendored Normal file
View file

@ -0,0 +1,8 @@
{
"recommendations": [
"eg2.vscode-npm-script",
"christian-kohler.npm-intellisense",
"ms-edgedevtools.vscode-edge-devtools",
"PWABuilder.pwa-studio"
]
}

29
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,29 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-msedge",
"request": "launch",
"name": "Run PWA",
"webRoot": "${workspaceFolder}/",
"runtimeArgs": [
"--app=http://localhost:5173"
],
"sourceMapPathOverrides": {
"../../src": "${workspaceFolder}/src",
"../../src/*": "${workspaceFolder}/src/*"
},
"preLaunchTask": "npm run dev-task",
"postDebugTask": "postdebugKill"
},
{
"name": "Launch Microsoft Edge and open the Edge DevTools",
"request": "launch",
"type": "vscode-edge-devtools.debug",
"url": "" // Provide your project's url to finish configuring
}
]
}

9
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,9 @@
{
"files.trimTrailingWhitespace": true,
"markdownlint.config": {
"MD028": false,
"MD025": {
"front_matter_title": ""
}
},
}

36
.vscode/tasks.json vendored Normal file
View file

@ -0,0 +1,36 @@
{
"version": "2.0.0",
"tasks": [
{
"label": "npm run dev-task",
"type": "npm",
"script": "dev-task",
"isBackground": true,
"problemMatcher": {
"owner": "custom",
"pattern": {
"regexp": "^$"
},
"background": {
"activeOnStart": true,
"beginsPattern": ".*",
"endsPattern": "ready in .+"
}
},
},
{
"label": "postdebugKill",
"command": "echo ${input:terminate}",
"type": "shell",
"problemMatcher": []
},
],
"inputs": [
{
"id": "terminate",
"type": "command",
"command": "workbench.action.tasks.terminate",
"args": "npm run dev-task"
}
]
}

13
LICENSE.txt Normal file
View file

@ -0,0 +1,13 @@
ManifoldJS
Copyright (c) Microsoft Corporation
All rights reserved.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the ""Software""), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

9
README.md Normal file
View file

@ -0,0 +1,9 @@
# Nostr Microclient
using nostr-tools and pwabuilder to play with notes and display them on an installable personal web page.
## Jump Right In
try installing to your smartphone home screen

62
index.html Normal file
View file

@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>fostr</title>
<base href="/" />
<!-- This meta viewport ensures the webpage's dimensions change according to the device it's on. This is called Responsive Web Design.-->
<meta name="viewport"
content="viewport-fit=cover, width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=5.0" />
<meta name="description" content="This is a fostr app" />
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#181818" />
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f3f3f3" />
<!-- These meta tags are Apple-specific, and set the web application to run in full-screen mode with a black status bar. Learn more at https://developer.apple.com/library/archive/documentation/AppleApplications/Reference/SafariHTMLRef/Articles/MetaTags.html-->
<meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-title" content="fostr" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<!-- This tag is used by the fostr CLI to identify template projects. Don't remove if you are using the CLI. -->
<meta name="pwa-starter-template-identity" content="pwa-starter"/>
<!-- Imports an icon to represent the document. -->
<link rel="icon" href="/assets/icons/icon_24.png" type="image/png" />
<!-- Imports the manifest to represent the web application. A web app must have a manifest to be a PWA. -->
<link rel="manifest" href="manifest.json" />
<!-- light mode and dark mode CSS -->
<link rel="stylesheet" media="(prefers-color-scheme:light)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/dist/themes/light.css">
<link rel="stylesheet" media="(prefers-color-scheme:dark)"
href="https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/dist/themes/dark.css"
onload="document.documentElement.classList.add('sl-theme-dark');">
<script type="module">
import { setBasePath } from '@shoelace-style/shoelace/dist/utilities/base-path.js';
setBasePath('https://cdn.jsdelivr.net/npm/@shoelace-style/shoelace@2.16.0/cdn/');
</script>
<script type="module" src="/src/app-index.ts"></script>
</head>
<body>
<!-- Our app-index web component. This component is defined in src/pages/app-index.ts-->
<app-index></app-index>
<script>
if ('serviceWorker' in navigator) {
window.onload = () => {
navigator.serviceWorker.register(
'/sw.js'
);
}
}
</script>
</body>
</html>

5360
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

43
package.json Normal file
View file

@ -0,0 +1,43 @@
{
"name": "fostr",
"version": "0.0.1",
"description": "A starter kit for building PWAs!",
"main": "index.js",
"scripts": {
"dev-server": "vite --open",
"dev": "npm run dev-server",
"dev-task": "vite",
"deploy": "npx @azure/static-web-apps-cli login --no-use-keychain && npx @azure/static-web-apps-cli deploy",
"build": "tsc && vite build",
"start": "npm run dev",
"start-remote": "vite --host"
},
"author": "",
"license": "ISC",
"dependencies": {
"@lit/localize": "^0.12.2",
"@shoelace-style/shoelace": "^2.18.0",
"@thepassle/app-tools": "^0.9.12",
"lit": "^3.2.1",
"nostr-tools": "^2.10.1",
"urlpattern-polyfill": "^10.0.0",
"workbox-build": "^7.3.0",
"workbox-core": "^7.3.0",
"workbox-precaching": "^7.3.0"
},
"devDependencies": {
"typescript": "^5.6.3",
"vite": "^5.4.11",
"vite-plugin-pwa": "^0.20.5"
},
"prettier": {
"tabWidth": 2,
"useTabs": false,
"semi": true,
"singleQuote": true,
"quoteProps": "consistent",
"trailingComma": "es5",
"endOfLine": "crlf",
"bracketSpacing": true
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 357 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 518 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

86
public/manifest.json Normal file
View file

@ -0,0 +1,86 @@
{
"id": "/",
"scope": "/",
"name": "fostr",
"display": "standalone",
"start_url": "/",
"short_name": "starter",
"theme_color": "#E1477E",
"description": "This is a fostr app",
"orientation": "any",
"background_color": "#E1477E",
"related_applications": [],
"prefer_related_applications": false,
"display_override": ["window-controls-overlay"],
"launch_handler": {
"client_mode": "focus-existing"
},
"icons": [
{
"src": "assets/icons/512x512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "assets/icons/192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "assets/icons/48x48.png",
"sizes": "48x48",
"type": "image/png"
},
{
"src": "assets/icons/24x24.png",
"sizes": "24x24",
"type": "image/png"
}
],
"screenshots": [
{
"src": "assets/screenshots/screen.png",
"sizes": "1617x1012",
"type": "image/png"
}
],
"features": [
"Cross Platform",
"fast",
"simple"
],
"categories": [
"social"
],
"shortcuts": [
{
"name": "Open About",
"short_name": "About",
"description": "Open the about page",
"url": "/about",
"icons": [{ "src": "assets/icons/192x192.png", "sizes": "192x192" }]
}
],
"widgets": [
{
"name": "Starter Widget",
"tag": "starterWidget",
"ms_ac_template": "widget/ac.json",
"data": "widget/data.json",
"description": "A simple widget example from pwa-starter.",
"screenshots": [
{
"src": "assets/screenshots/widget-screen.png",
"sizes": "500x500",
"label": "Widget screenshot"
}
],
"icons": [
{
"src": "assets/icons/48x48.png",
"sizes": "48x48"
}
]
}
]
}

View file

@ -0,0 +1,6 @@
{
"navigationFallback": {
"rewrite": "index.html",
"exclude": ["*.{css,js,mjs,ts,png,gif,ico,jpg,svg,json,woff2,ttf}"]
}
}

60
public/sw.js Normal file
View file

@ -0,0 +1,60 @@
importScripts(
'https://storage.googleapis.com/workbox-cdn/releases/7.3.0/workbox-sw.js'
);
// This is your Service Worker, you can put any of your custom Service Worker
// code in this file, above the `precacheAndRoute` line.
// When widget is installed/pinned, push initial state.
self.addEventListener('widgetinstall', (event) => {
event.waitUntil(updateWidget(event));
});
// When widget is shown, update content to ensure it is up-to-date.
self.addEventListener('widgetresume', (event) => {
event.waitUntil(updateWidget(event));
});
// When the user clicks an element with an associated Action.Execute,
// handle according to the 'verb' in event.action.
self.addEventListener('widgetclick', (event) => {
if (event.action == "updateName") {
event.waitUntil(updateName(event));
}
});
// When the widget is uninstalled/unpinned, clean up any unnecessary
// periodic sync or widget-related state.
self.addEventListener('widgetuninstall', (event) => {});
const updateWidget = async (event) => {
// The widget definition represents the fields specified in the manifest.
const widgetDefinition = event.widget.definition;
// Fetch the template and data defined in the manifest to generate the payload.
const payload = {
template: JSON.stringify(await (await fetch(widgetDefinition.msAcTemplate)).json()),
data: JSON.stringify(await (await fetch(widgetDefinition.data)).json()),
};
// Push payload to widget.
await self.widgets.updateByInstanceId(event.instanceId, payload);
}
const updateName = async (event) => {
const name = event.data.json().name;
// The widget definition represents the fields specified in the manifest.
const widgetDefinition = event.widget.definition;
// Fetch the template and data defined in the manifest to generate the payload.
const payload = {
template: JSON.stringify(await (await fetch(widgetDefinition.msAcTemplate)).json()),
data: JSON.stringify({name}),
};
// Push payload to widget.
await self.widgets.updateByInstanceId(event.instanceId, payload);
}
workbox.precaching.precacheAndRoute(self.__WB_MANIFEST || []);

24
public/widget/ac.json Normal file
View file

@ -0,0 +1,24 @@
{
"type": "AdaptiveCard",
"body": [
{
"type": "TextBlock",
"text": "Hello ${$root.name}!",
"wrap": true,
"horizontalAlignment": "Center",
"size": "ExtraLarge"
},
{
"type": "Input.Text",
"placeholder": "Name",
"id": "name",
"inlineAction": {
"type": "Action.Execute",
"verb": "updateName",
"title": "Submit"
}
}
],
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.6"
}

3
public/widget/data.json Normal file
View file

@ -0,0 +1,3 @@
{
"name": "Widget"
}

37
src/app-index.ts Normal file
View file

@ -0,0 +1,37 @@
import { LitElement, css } from 'lit';
import { customElement } from 'lit/decorators.js';
import './pages/note-wall'
import './pages/app-home';
import './components/header';
import './styles/global.css';
import { router } from './router';
@customElement('app-index')
export class AppIndex extends LitElement {
static styles = css`
main {
padding-left: 16px;
padding-right: 16px;
padding-bottom: 16px;
}
`;
firstUpdated() {
router.addEventListener('route-changed', () => {
if ("startViewTransition" in document) {
(document as any).startViewTransition(() => this.requestUpdate());
}
else {
this.requestUpdate();
}
});
}
render() {
// router config can be round in src/router.ts
return router.render();
}
}

166
src/components/header.ts Normal file
View file

@ -0,0 +1,166 @@
import { LitElement, css, html } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import { resolveRouterPath } from '../router';
import { Relay } from 'nostr-tools';
import { WindowNostr } from 'nostr-tools/nip07';
import '@shoelace-style/shoelace/dist/components/button/button.js';
@customElement('app-header')
export class AppHeader extends LitElement {
@property({ type: String }) title = 'fostr';
@property({ type: Boolean}) enableBack: boolean = false;
@property({ type: String }) nostrAddy = '';
@property({ type: String }) bio = '';
@property({ type: String }) profilePic = '';
@property({ type: Boolean }) isSignedIn = false;
@property({ type: String }) publicKey = '';
static styles = css`
header {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--app-color-primary);
color: white;
padding: 12px;
padding-top: 4px;
position: fixed;
left: env(titlebar-area-x, 0);
top: env(titlebar-area-y, 0);
height: env(titlebar-area-height, 30px);
width: env(titlebar-area-width, 100%);
-webkit-app-region: drag;
}
header h1 {
margin-top: 0;
margin-bottom: 0;
font-size: 12px;
font-weight: bold;
}
nav a {
margin-left: 10px;
}
#back-button-block {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
}
@media(prefers-color-scheme: light) {
header {
color: black;
}
nav a {
color: initial;
}
}
`;
connectedCallback(): void {
super.connectedCallback();
//is there a stored pubkey in localStorage already?
const storedPubkey = localStorage.getItem('pubkey');
if (storedPubkey) {
this.isSignedIn = true;
this.fetchProfileMetadata(storedPubkey);
} else {
// Show guest view by default
this.displayGuestView();
}}
displayGuestView() {
// 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 = ''; // Could be a placeholder image for guests
this.isSignedIn = false;
}
async fetchProfileMetadata(pubkey: string) {
const relay = await Relay.connect('wss://notes.miguelalmodo.com');
console.log(`connected to ${relay.url}`);
// subscribe to my profile metadata from the relay
const sub = relay.subscribe([
{
kinds: [0], // profile metadata
authors: [pubkey], //will insert the pubkey of the user who signed in
}
], {
onevent: (event) => {
const profileData = JSON.parse(event.content);
this.profilePic = profileData.picture || '';
this.nostrAddy = profileData.nip05 || '';
this.bio = profileData.about || '';
this.dispatchEvent(new CustomEvent('profile-updated' , {
detail: { nostrAddy: this.nostrAddy, bio: this.bio, profilePic: this.profilePic },
bubbles: true,
composed: true
}));
},
oneose: () => {
sub.close();
}
});
}
async signInWithNostr() {
if (window.nostr) {
try {
const userPubkey = await window.nostr.getPublicKey();
this.isSignedIn = true;
//save pubkey in localStorage to keep sign-in state
localStorage.setItem('pubkey', userPubkey)
await this.fetchProfileMetadata(userPubkey);
} catch (error) {
console.error('Failed to sign in:', error);
}
} else {
alert('Nostr extension not detected. Please install a Nostr extension.');
}
}
signOut() {
// clear pubkey from localStorage and reset to guest view
localStorage.removeItem('pubKey');
this.displayGuestView();
console.log('signed out')
this.dispatchEvent( new CustomEvent('user-signed-out', {
bubbles: true,
composed: true
}));
}
render() {
return html`
<header>
<div id="back-button-block">
${this.enableBack ? html`<sl-button size="small" href="${resolveRouterPath()}">
Back
</sl-button>` : null}
<h1>${this.title}</h1>
</div>
<sl-button variant="primary" @click="${this.isSignedIn ? this.signOut : this.signInWithNostr}">
${this.isSignedIn ? 'Sign out' : 'Sign in with Nostr'}
</sl-button>
</header>
`;
}
}

BIN
src/img/default_pfp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -0,0 +1,11 @@
import { css } from 'lit';
// these styles can be imported from any component
// for an example of how to use this, check /pages/about-about.ts
export const styles = css`
@media(min-width: 1000px) {
sl-card {
max-width: 70vw;
}
}
`;

View file

@ -0,0 +1,84 @@
import { LitElement, html, css } from 'lit';
import { customElement } from 'lit/decorators.js';
// You can also import styles from another file
// if you prefer to keep your CSS seperate from your component
import { styles } from './about-styles';
import { styles as sharedStyles } from '../../styles/shared-styles'
import '@shoelace-style/shoelace/dist/components/card/card.js';
@customElement('app-about')
export class AppAbout extends LitElement {
static styles = [
sharedStyles,
styles,
css`
#welcomeBar {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#welcomeCard,
#infoCard {
padding: 18px;
padding-top: 0px;
}
sl-card::part(footer) {
display: flex;
justify-content: flex-end;
}
@media(min-width: 750px) {
sl-card {
width: 70vw;
}
}
`
]
render() {
return html`
<app-header ?enableBack="${true}"></app-header>
<main><div id="welcomeBar">
<h2>About Page</h2>
<sl-card id="welcomeCard">
<h2>Did you know?</h2>
<p>PWAs have access to many useful APIs in modern browsers! These
APIs have enabled many new types of apps that can be built as PWAs, such as advanced graphics editing apps, games,
apps that use machine learning and more!
</p>
<p>Check out <a
href="https://docs.microsoft.com/en-us/microsoft-edge/progressive-web-apps-chromium/how-to/handle-files">these
docs</a> to learn more about the advanced features that you can use in your PWA</p>
<hr>
<h2>Technology Used</h2>
<ul>
<li>
<a href="https://www.npmjs.com/package/nostr-tools?activeTab=dependencies">Nostr Tools</a>
</li>
<li>
<a href="https://lit.dev">Web Components</a>
</li>
<li>
<a href="https://github.com/hoytech/strfry"
>Strfry</a>
</li>
</ul>
</sl-card></div>
</main>
`;
}
}

218
src/pages/app-home.ts Normal file
View file

@ -0,0 +1,218 @@
import { LitElement, css, html } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import { resolveRouterPath } from '../router';
import { Relay } from 'nostr-tools';
import { WindowNostr } from 'nostr-tools/nip07';
import '../img/default_pfp.png'
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import { styles } from '../styles/shared-styles';
declare global {
interface Window {
nostr?: WindowNostr;
}
}
@customElement('app-home')
export class AppHome extends LitElement {
// For more information on using properties and state in lit
// check out this link https://lit.dev/docs/components/properties/
@property() message = 'Welcome to my nostr demo!';
@property({ type: String }) nostrAddy = '';
@property({ type: String }) bio = '';
@property({ type: String }) profilePic = '';
@property({ type: Boolean }) isSignedIn = false;
@property({ type: String }) publicKey = '';
static styles = [
styles,
css`
#welcomeBar {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#welcomeCard,
#infoCard {
padding: 18px;
padding-top: 0px;
}
sl-card::part(footer) {
display: flex;
justify-content: flex-end;
}
@media(min-width: 750px) {
sl-card {
width: 70vw;
}
}
@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 {
display: grid;
grid-template-columns: 200px 115px;
grid-template-rows: 250px;
grid-column-gap: 15px;
margin-bottom: 12px;
}
.profile-picture-container p {
margin: 0;
}
.profile-pic {
grid-area: 1/1;
margin-top: 10px;
margin-bottom: 10px;
}
.personal-msg {
grid-area: 1/2;
margin: 0;
}
.pics-videos {
grid-area: 1/1;
justify-self: center;
align-self: end;
}
`];
async firstUpdated() {
// this method is a lifecycle even in lit
// for more info check out the lit docs https://lit.dev/docs/components/lifecycle/
console.log('This is your home page');
}
connectedCallback(): void {
super.connectedCallback();
this.addEventListener('user-signed-out', this.handlesSignOut.bind(this))
}
handlesSignOut() {
this.profilePic = '';
this.bio = '';
this.nostrAddy = 'Guest'
}
disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener('profile-updated', this.updateProfileFromEvent.bind(this));
}
updateProfileFromEvent(event: Event) {
const detail = (event as CustomEvent).detail;
this.nostrAddy = detail.nostrAddy;
this.bio = detail.bio;
this.profilePic = detail.profilePic;
this.isSignedIn = true;
this.requestUpdate();
}
displayGuestView() {
// 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 = ''; // Could be a placeholder image for guests
this.isSignedIn = false;
}
share() {
if ((navigator as any).share) {
(navigator as any).share({
title: 'PWABuilder pwa-starter',
text: 'Check out the PWABuilder pwa-starter!',
url: 'https://github.com/pwa-builder/pwa-starter',
});
}
}
render() {
return html`
<app-header></app-header>
<main>
<div id="welcomeBar">
<sl-card id="welcomeCard">
<div slot="header">
<h2>Find me on any nostr client by searching ${this.nostrAddy || 'Guest'}</h2>
</div>
<p>
You can use the Nostr protocol to transform your personal website into
a micro social media client. It eliminates the need to use a corporate
social media account. I am using nostr-tools to implement an extension
sign in and pull notes from my personal relay.
</p>
<div class="profile-picture-container">
${this.profilePic
? html`<img class="profile-pic" src="${this.profilePic}" alt="Profile Picture" width="200px" height="200px">`
: html`<img class="profile-pic" src="img/default_pfp.png" alt="Guest 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="pics-videos">View My: <a href="#"><b>Pics</b></a> | <a href="#"><b>Videos</b></a></p>
</div>
${'share' in navigator
? html`<sl-button slot="footer" variant="default" @click="${this.share}">
<sl-icon slot="prefix" name="share"></sl-icon>
Share this Starter!
</sl-button>`
: null}
</sl-card>
<sl-button href="${resolveRouterPath('about')}" variant="primary">Navigate to About</sl-button>
<sl-button href="${resolveRouterPath('note-wall')}" variant="primary">Navigate to Note Wall</sl-button>
</div>
</main>
`;
}
}

243
src/pages/note-wall.ts Normal file
View file

@ -0,0 +1,243 @@
import { LitElement, css, html } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import { resolveRouterPath } from '../router';
import { Relay } from 'nostr-tools';
import '@shoelace-style/shoelace/dist/components/card/card.js';
import '@shoelace-style/shoelace/dist/components/button/button.js';
import { styles } from '../styles/shared-styles';
@customElement('note-wall')
export class NoteWall extends LitElement {
@property({ type: Array }) notes: { content: string; date: string }[] = [];
static styles = [
styles,
css`
#welcomeBar {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
#welcomeCard,
#infoCard {
padding: 18px;
padding-top: 0px;
}
sl-card::part(footer) {
display: flex;
justify-content: flex-end;
}
@media(min-width: 750px) {
sl-card {
width: 70vw;
}
}
@media (horizontal-viewport-segments: 2) {
#welcomeBar {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
#welcomeCard {
margin-right: 64px;
}
}
@media(min-width: 750px) {
sl-card {
width: 70vw;
}
}
@media (horizontal-viewport-segments: 2) {
#welcomeBar {
flex-direction: row;
align-items: flex-start;
justify-content: space-between;
}
}
.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;
}
.comment-wall th {
background-color: rgb(255, 153, 51);
width: 158px;
padding: 3px;
vertical-align: top;
}
.comment-wall td {
vertical-align: top;
background-color: rgb(249, 214, 180);
width: 269px;
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: 1em;
}
.comment-wall p {
font-weight: normal;
text-align: left;
margin: 0;
}
#add-comment {
text-align: right;
margin-right: 10px;
margin-bottom: 5px;
}
`];
note = ''; // store notes
async firstUpdated() {
// this method is a lifecycle even in lit
// for more info check out the lit docs https://lit.dev/docs/components/lifecycle/
console.log('Here are some recent notes from the neighborhood');
}
async connectedCallback(): Promise<void> {
super.connectedCallback();
await Promise.all([
this.fetchNotes(),
]);
}
async fetchNotes() {
const ifcaRelay = await Relay.connect('wss://hi.myvoiceourstory.org');
const migsRelay = await Relay.connect('wss://notes.miguelalmodo.com');
console.log(`connected to ${ifcaRelay.url}`);
console.log(`connected to ${migsRelay.url}`);
const eric = 'f8d17812ce41a9b145aede4f0720050ab63607f51688832b5d78c65d620ec7d9'
const migs = 'ec965405e11a6a6186b27fa451a2ffc1396ede7883d2ea11c32fbd2c63996966'
const sub = ifcaRelay.subscribe([
{
kinds: [1], // short form notes
limit: 20,
authors: [eric]
}
], {
onevent: (event) => {
this.notes.push({
content: event.content,
date: new Date(event.created_at * 1000).toLocaleDateString()
});
this.requestUpdate();
console.log(event)
},
oneose: () => {
sub.close();
this.requestUpdate();
}
});
}
share() {
if ((navigator as any).share) {
(navigator as any).share({
title: 'Nostr Micro Client',
text: 'Share this with your homie!',
url: 'https://miguelalmodo.com',
});
}
}
render() {
return html`
<app-header ?enableBack="${true}"></app-header>
<main> <div id="welcomeBar">
<sl-card id="WelcomeCard">
<section class=""comment-wall>
<header class="main-section-header">
<h2 class="main-section-h2">Recent Notes from IFCA</h2>
</header>
<table>
${this.notes.map(note => {
// Check if there's a URL in the content
const urlMatch = note.content.match(/https?:\/\/[^\s]+/);
const imageUrl = urlMatch ? urlMatch[0] : null;
const textContent = note.content.replace(urlMatch?.[0] || '', '').trim();
return html`
<tr>
<td><h3>${note.date}</h3></td>
<td>
<p>${textContent}</p>
${imageUrl ? html`<img src="${imageUrl}" alt="Note image" style="max-width: 30%; height: 30%;">` : ''}
</td>
</tr>
`;
})}
</table>
</section></main>
</sl-card>
`;
}
}

53
src/router.ts Normal file
View file

@ -0,0 +1,53 @@
// docs for router https://github.com/thepassle/app-tools/blob/master/router/README.md
import { html } from 'lit';
if (!(globalThis as any).URLPattern) {
await import("urlpattern-polyfill");
}
import { Router } from '@thepassle/app-tools/router.js';
import { lazy } from '@thepassle/app-tools/router/plugins/lazy.js';
// @ts-ignore
import { title } from '@thepassle/app-tools/router/plugins/title.js';
import './pages/app-home.js';
const baseURL: string = (import.meta as any).env.BASE_URL;
export const router = new Router({
routes: [
{
path: resolveRouterPath(),
title: 'Home',
render: () => html`<app-home></app-home>`
},
{
path: resolveRouterPath('about'),
title: 'About',
plugins: [
lazy(() => import('./pages/app-about/app-about.js')),
],
render: () => html`<app-about></app-about>`
},
{
path: resolveRouterPath('note-wall'),
title: 'Note Wall',
render: () => html`<note-wall></note-wall>`
}
]
});
// This function will resolve a path with whatever Base URL was passed to the vite build process.
// Use of this function throughout the starter is not required, but highly recommended, especially if you plan to use GitHub Pages to deploy.
// If no arg is passed to this function, it will return the base URL.
export function resolveRouterPath(unresolvedPath?: string) {
var resolvedPath = baseURL;
if(unresolvedPath) {
resolvedPath = resolvedPath + unresolvedPath;
}
return resolvedPath;
}

34
src/styles/global.css Normal file
View file

@ -0,0 +1,34 @@
/*
This file is used for all of your global styles and CSS variables.
Check here https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties for more info on using CSS variables.
*/
:root {
--font-family: sans-serif;
}
html, body {
font-family: var(--font-family);
padding: 0;
margin: 0;
height: 100%;
}
@media (prefers-color-scheme: dark) {
html,
body {
background-color: #181818;
color: white;
}
}
@media (prefers-color-scheme: light) {
html,
body {
background-color: #f5f5f5;
color: black;
}
}

View file

@ -0,0 +1,16 @@
import { css } from 'lit';
// these styles can be imported from any component
// for an example of how to use this, check /pages/about-about.ts
export const styles = css`
@media(min-width: 1000px) {
sl-card {
max-width: 70vw;
}
}
main {
margin-top: 34px;
padding: 12px;
}
`;

12
swa-cli.config.json Normal file
View file

@ -0,0 +1,12 @@
{
"$schema": "https://aka.ms/azure/static-web-apps-cli/schema",
"configurations": {
"fostr": {
"appLocation": ".",
"outputLocation": "dist",
"appBuildCommand": "npm run build --if-present",
"run": "npm start",
"appDevserverUrl": "http://localhost:3000"
}
}
}

28
tsconfig.json Normal file
View file

@ -0,0 +1,28 @@
{
"compilerOptions": {
"module": "esnext",
"target": "esnext",
"lib": ["es2017", "esnext", "dom", "dom.iterable"],
"declaration": true,
"emitDeclarationOnly": true,
"skipLibCheck": true,
"outDir": "./types",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true,
"experimentalDecorators": true,
"forceConsistentCasingInFileNames": true,
"useDefineForClassFields": false,
"isolatedModules": true,
"types": [
"vite-plugin-pwa/client",
"vite/client"
]
},
"include": ["src/**/*.ts", "router.d.ts"],
"exclude": ["node_modules"]
}

32
vite.config.ts Normal file
View file

@ -0,0 +1,32 @@
import { defineConfig } from 'vite';
import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
base: "/",
build: {
sourcemap: true,
assetsDir: "code",
target: ["esnext"],
cssMinify: true,
lib: false
},
plugins: [
VitePWA({
strategies: "injectManifest",
injectManifest: {
swSrc: 'public/sw.js',
swDest: 'dist/sw.js',
globDirectory: 'dist',
globPatterns: [
'**/*.{html,js,css,json,png}',
],
},
injectRegister: false,
manifest: false,
devOptions: {
enabled: true
}
})
]
})