initial commit okayyy
8
.gitignore
vendored
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dev-dist
|
||||||
|
build
|
||||||
|
types
|
||||||
|
.idea
|
||||||
|
.github
|
8
.vscode/extensions.json
vendored
Normal 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
|
@ -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
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"files.trimTrailingWhitespace": true,
|
||||||
|
"markdownlint.config": {
|
||||||
|
"MD028": false,
|
||||||
|
"MD025": {
|
||||||
|
"front_matter_title": ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
36
.vscode/tasks.json
vendored
Normal 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
|
@ -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
|
@ -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
|
@ -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
43
package.json
Normal 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
|
||||||
|
}
|
||||||
|
}
|
BIN
public/assets/icons/192x192.png
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
public/assets/icons/24x24.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/assets/icons/48x48.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
public/assets/icons/512x512.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
public/assets/icons/icon_192.png
Normal file
After Width: | Height: | Size: 20 KiB |
BIN
public/assets/icons/icon_24.png
Normal file
After Width: | Height: | Size: 1.7 KiB |
BIN
public/assets/icons/icon_48.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
public/assets/icons/icon_512.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
public/assets/readme/build-output.png
Normal file
After Width: | Height: | Size: 107 KiB |
BIN
public/assets/readme/codespace-button.png
Normal file
After Width: | Height: | Size: 283 KiB |
BIN
public/assets/readme/copy-starter.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
public/assets/readme/git-clone.png
Normal file
After Width: | Height: | Size: 85 KiB |
BIN
public/assets/readme/intro.png
Normal file
After Width: | Height: | Size: 357 KiB |
BIN
public/assets/readme/local-button.png
Normal file
After Width: | Height: | Size: 164 KiB |
BIN
public/assets/readme/new-repo-from-starter.png
Normal file
After Width: | Height: | Size: 155 KiB |
BIN
public/assets/readme/pwa-running.png
Normal file
After Width: | Height: | Size: 518 KiB |
BIN
public/assets/readme/pwa-starter-overview.png
Normal file
After Width: | Height: | Size: 319 KiB |
BIN
public/assets/readme/static-web-app-slash.png
Normal file
After Width: | Height: | Size: 125 KiB |
BIN
public/assets/readme/use-this-template.png
Normal file
After Width: | Height: | Size: 76 KiB |
BIN
public/assets/readme/vscode-in-browser.png
Normal file
After Width: | Height: | Size: 427 KiB |
BIN
public/assets/screenshots/screen.png
Normal file
After Width: | Height: | Size: 51 KiB |
BIN
public/assets/screenshots/widget-screen.png
Normal file
After Width: | Height: | Size: 22 KiB |
86
public/manifest.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
6
public/staticwebapp.config.json
Normal 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
|
@ -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
|
@ -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
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"name": "Widget"
|
||||||
|
}
|
37
src/app-index.ts
Normal 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
|
@ -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
After Width: | Height: | Size: 30 KiB |
11
src/pages/app-about/about-styles.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`;
|
84
src/pages/app-about/app-about.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
16
src/styles/shared-styles.ts
Normal 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
|
@ -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
|
@ -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
|
@ -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
|
||||||
|
}
|
||||||
|
})
|
||||||
|
]
|
||||||
|
})
|