diff --git a/Dockerfile b/Dockerfile
index dc3287a..f5feb6e 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -25,12 +25,7 @@ COPY src/ ./src/
COPY src/pages/ ./src/pages/
COPY src/emails/ ./src/emails/
-# Build web UI
-COPY web/package.json web/pnpm-lock.yaml web/pnpm-workspace.yaml web/tsconfig.json web/vite.config.js web/index.html ./web/
-COPY web/src/ ./web/src/
-RUN cd web && pnpm install --frozen-lockfile && pnpm run build
-
-# Build TypeScript (runs tsc && build:html && build:web)
+# Build TypeScript (runs tsc && build:html)
RUN pnpm run build
# Production image
@@ -51,7 +46,6 @@ RUN pnpm install --frozen-lockfile --prod
# Copy build artifacts
COPY --from=build /app/dist/ ./dist/
-COPY --from=build /app/web/dist/ ./web/dist/
COPY --from=build /app/src/pages/ ./dist/pages/
COPY --from=build /app/src/emails/ ./dist/emails/
diff --git a/build-in-production.sh b/build-in-production.sh
old mode 100755
new mode 100644
index 37bf10d..639fba3
--- a/build-in-production.sh
+++ b/build-in-production.sh
@@ -2,11 +2,9 @@
# Remove link overrides
node remove-pnpm-overrides.js package.json
-node remove-pnpm-overrides.js web/package.json
# When CI=true as it is on render.com, removing link overrides breaks the lockfile
pnpm i --no-frozen-lockfile
-(cd web && pnpm i --no-frozen-lockfile)
# Build everything
pnpm run build
diff --git a/package.json b/package.json
index b2b588f..9ac4de2 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,7 @@
"type": "module",
"version": "1.0.0",
"scripts": {
- "build": "tsc && pnpm run build:html && pnpm run build:web",
- "build:web": "cd web && pnpm run build",
+ "build": "tsc && pnpm run build:html",
"build:html": "cp -r src/pages dist/ && cp -r src/emails dist/",
"check": "tsc --noEmit && eslint src",
"format": "prettier --write \"src/**/*.{ts,js,json,html}\"",
diff --git a/src/server.ts b/src/server.ts
index eb3fe92..4e6e8fa 100644
--- a/src/server.ts
+++ b/src/server.ts
@@ -33,8 +33,6 @@ server.use((req: Request, res: Response, next: NextFunction) => {
server.use(express.json())
-server.use(express.static('web/dist'))
-
// Rate limit for registration endpoints
server.use(
'/subscription',
@@ -71,16 +69,6 @@ const addRoute = (method: 'get' | 'post' | 'put' | 'delete', path: string, handl
}
addRoute('get', '/', async (req: Request, res: Response) => {
- try {
- const {existsSync} = await import('fs')
- const webIndex = new URL('../web/dist/index.html', import.meta.url)
- if (existsSync(webIndex)) {
- return res.send(await render('../web/dist/index.html'))
- }
- } catch {
- // Fall through to JSON
- }
-
res.json({
name: 'Mailship',
description: 'Email notification server for Nostr',
diff --git a/web/.env.template b/web/.env.template
deleted file mode 100644
index 9f0b1f1..0000000
--- a/web/.env.template
+++ /dev/null
@@ -1,3 +0,0 @@
-VITE_NOTIFIER_PUBKEY=
-VITE_NOTIFIER_RELAY=
-VITE_INDEXER_RELAYS=purplepag.es,relay.damus.io,relay.nostr.band
diff --git a/web/.gitignore b/web/.gitignore
deleted file mode 100644
index a547bf3..0000000
--- a/web/.gitignore
+++ /dev/null
@@ -1,24 +0,0 @@
-# Logs
-logs
-*.log
-npm-debug.log*
-yarn-debug.log*
-yarn-error.log*
-pnpm-debug.log*
-lerna-debug.log*
-
-node_modules
-dist
-dist-ssr
-*.local
-
-# Editor directories and files
-.vscode/*
-!.vscode/extensions.json
-.idea
-.DS_Store
-*.suo
-*.ntvs*
-*.njsproj
-*.sln
-*.sw?
diff --git a/web/eslint.config.js b/web/eslint.config.js
deleted file mode 100644
index d785258..0000000
--- a/web/eslint.config.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import js from "@eslint/js";
-import globals from "globals";
-import tseslint from "typescript-eslint";
-import { defineConfig } from "eslint/config";
-
-
-export default defineConfig([
- { files: ["**/*.{js,mjs,cjs,ts}"], plugins: { js }, extends: ["js/recommended"] },
- { files: ["**/*.{js,mjs,cjs,ts}"], languageOptions: { globals: globals.browser } },
- tseslint.configs.recommended,
- {
- files: ["src/**/*.{js,mjs,cjs,ts}"],
- rules: {
- "@typescript-eslint/no-explicit-any": "off",
- "@typescript-eslint/no-unused-vars": [
- "error",
- {args: "none", destructuredArrayIgnorePattern: "^_d?$", caughtErrors: "none"},
- ],
- },
- },
-]);
diff --git a/web/index.html b/web/index.html
deleted file mode 100644
index b477b77..0000000
--- a/web/index.html
+++ /dev/null
@@ -1,15 +0,0 @@
-
-
-
-
-
-
- Anchor Alerts
-
-
-
-
-
-
diff --git a/web/package.json b/web/package.json
deleted file mode 100644
index c783cf3..0000000
--- a/web/package.json
+++ /dev/null
@@ -1,34 +0,0 @@
-{
- "name": "web",
- "private": true,
- "version": "0.0.0",
- "type": "module",
- "scripts": {
- "dev": "vite",
- "build": "tsc && vite build",
- "check": "tsc --noEmit && eslint src",
- "format": "eslint src --fix"
- },
- "devDependencies": {
- "@eslint/js": "^9.25.1",
- "@types/mithril": "^2.2.7",
- "eslint": "^9.25.1",
- "globals": "^16.0.0",
- "typescript": "~5.7.2",
- "typescript-eslint": "^8.31.1",
- "vite": "^6.3.1"
- },
- "dependencies": {
- "@tailwindcss/vite": "^4.1.4",
- "@welshman/feeds": "^0.6.3",
- "@welshman/lib": "^0.6.3",
- "@welshman/net": "^0.6.3",
- "@welshman/signer": "^0.6.3",
- "@welshman/store": "^0.6.3",
- "@welshman/util": "^0.6.3",
- "events": "^3.3.0",
- "mithril": "^2.2.15",
- "svelte": "^5.27.2",
- "tailwindcss": "^4.1.4"
- }
-}
diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml
deleted file mode 100644
index dc6d6ad..0000000
Binary files a/web/pnpm-lock.yaml and /dev/null differ
diff --git a/web/pnpm-workspace.yaml b/web/pnpm-workspace.yaml
deleted file mode 100644
index 6810e3a..0000000
--- a/web/pnpm-workspace.yaml
+++ /dev/null
@@ -1,2 +0,0 @@
-allowBuilds:
- esbuild: true
\ No newline at end of file
diff --git a/web/src/main.ts b/web/src/main.ts
deleted file mode 100644
index 13124a3..0000000
--- a/web/src/main.ts
+++ /dev/null
@@ -1,544 +0,0 @@
-import './style.css'
-
-import m from "mithril"
-import {writable} from 'svelte/store'
-import {getJson, removeNil, spec, parseJson, setJson, assoc, randomId, TIMEZONE, tryCatch, LOCALE} from '@welshman/lib'
-import {withGetter} from '@welshman/store'
-import {Router} from '@welshman/router'
-import {validateFeed, ValidationError, displayFeeds, Feed} from '@welshman/feeds'
-import {getAddress, getRelaysFromList, RelayMode, readList, asDecryptedEvent, normalizeRelayUrl, getTagValue, getTagValues, makeEvent, DELETE, TrustedEvent, StampedEvent, FEED, Address, getIdFilters, fromNostrURI, RELAYS} from '@welshman/util'
-import {load, publish, defaultSocketPolicies, makeSocketPolicyAuth} from '@welshman/net'
-import type {ISigner} from '@welshman/signer'
-import {Nip07Signer, decrypt} from '@welshman/signer'
-
-// Constants
-
-const NOTIFIER_PUBKEY = import.meta.env.VITE_NOTIFIER_PUBKEY
-
-const NOTIFIER_RELAY = import.meta.env.VITE_NOTIFIER_RELAY ? normalizeRelayUrl(import.meta.env.VITE_NOTIFIER_RELAY) : undefined
-
-const INDEXER_RELAYS = (import.meta.env.VITE_INDEXER_RELAYS || 'purplepag.es,relay.damus.io,relay.nostr.band').split(',').map(normalizeRelayUrl)
-
-const ALERT = 32830
-
-const ALERT_STATUS = 32831
-
-const TZ_OFFSET = parseInt(TIMEZONE.split(':')[0]!)
-
-const CRON_DAILY_PATTERN = /^0 \d{1,2} \d{1,2} \* \* \*$/
-
-const CRON_WEEKLY_PATTERN = /^0 \d{1,2} \d{1,2} \* \* 1$/
-
-const PLUS_ICON = `
-`
-
-const TRASH_ICON = `
-`
-
-const ARROW_LEFT_ICON = `
-`
-
-// Types and state
-
-type Alert = {
- event: TrustedEvent
- tags: string[][]
-}
-
-type AlertStatus = {
- event: TrustedEvent
- tags: string[][]
-}
-
-type AlertValues = {
- feedAddress: string
- freq: string
- time: string,
- email: string
- secret: string
-}
-
-type State = {
- failedToLogin: boolean
- signer: ISigner
- pubkey: string | undefined
- alerts: Alert[]
- alertDraft?: AlertValues,
- alertStatuses: AlertStatus[]
- alertsLoading: boolean
-}
-
-const state = withGetter(
- writable({
- failedToLogin: false,
- signer: new Nip07Signer(),
- pubkey: getJson('pubkey'),
- alerts: [],
- alertStatuses: [],
- alertsLoading: false,
- } as State)
-)
-
-// Actions
-
-const login = async () => {
- const {signer} = state.get()
-
- try {
- const pubkey = await signer.getPubkey()
-
- state.update(assoc('pubkey', pubkey))
- setJson('pubkey', pubkey)
- } catch (e) {
- state.update(assoc('failedToLogin', true))
- }
-}
-
-const loadAlerts = async () => {
- const {signer, pubkey} = state.get()
-
- if (!NOTIFIER_RELAY) {
- state.update(assoc('alertsLoading', false))
- return
- }
-
- state.update(assoc('alertsLoading', true))
-
- const events = await load({
- relays: [NOTIFIER_RELAY],
- filters: [
- {kinds: [ALERT], authors: [pubkey!]},
- {kinds: [ALERT_STATUS], "#p": [pubkey!]},
- ],
- })
-
- const alerts = await Promise.all(
- events
- .filter(spec({kind: ALERT}))
- .map(async event => {
- const tags = parseJson(await decrypt(signer, NOTIFIER_PUBKEY, event.content))
-
- return {event, tags}
- })
- )
-
- const alertStatuses = await Promise.all(
- events
- .filter(spec({kind: ALERT_STATUS}))
- .map(async event => {
- const tags = parseJson(await decrypt(signer, NOTIFIER_PUBKEY, event.content))
-
- return {event, tags}
- })
- )
-
- state.update($state => ({...$state, alertsLoading: false, alerts, alertStatuses}))
-}
-
-const deleteAlert = async (alert: Alert) => {
- if (!NOTIFIER_RELAY) return
-
- if (confirm("Are you sure you want to delete this alert?")) {
- state.update(assoc('alertsLoading', true))
-
- await publish({
- relays: [NOTIFIER_RELAY],
- event: await state.get().signer!.sign(
- makeEvent(DELETE, {
- tags: [
- ["k", String(alert.event.kind)],
- ["a", getAddress(alert.event)]
- ],
- })
- ),
- })
-
- await loadAlerts()
- }
-}
-
-export type AlertParams = {
- feeds: Feed[]
- freq: string
- time: string
- email: string
- secret: string
-}
-
-export const makeAlert = async ({freq, time, email, feeds, secret}: AlertParams) => {
- const {signer} = state.get()
- const [hour, minute] = time.split(':')
- const utcHour = (parseInt(hour) - TZ_OFFSET) % 24
- const dow = freq === 'daily' ? '*' : freq
- const cron = `0 ${minute} ${utcHour} * * ${dow}`
-
- const tags = [
- ["cron", cron],
- ["email", email],
- ["channel", "email"],
- ["locale", LOCALE],
- ["timezone", TIMEZONE],
- [
- "handler",
- "31990:97c70a44366a6535c145b333f973ea86dfdc2d7a99da618c40c64705ad98e322:1685968093690",
- "wss://relay.nostr.band/",
- "web",
- ],
- ]
-
- for (const feed of feeds) {
- tags.push(["feed", JSON.stringify(feed)])
- }
-
- return signer.sign(
- makeEvent(ALERT, {
- content: await signer.nip44.encrypt(NOTIFIER_PUBKEY, JSON.stringify(tags)),
- tags: [
- ["d", randomId()],
- ["p", NOTIFIER_PUBKEY],
- ],
- })
- )
-}
-
-export const publishAlert = async (params: AlertParams) => {
- if (!NOTIFIER_RELAY) return
-
- await publish({event: await makeAlert(params), relays: [NOTIFIER_RELAY]})
-}
-
-// Components
-
-const Loader = {
- view: () => m("div", { class: "flex justify-center py-4" }, [
- m("div", {
- class: "animate-spin rounded-full h-8 w-8 border-4 border-purple-200 border-t-purple-600"
- })
- ])
-}
-
-const Login = {
- view: () =>
- m("button", {
- onclick: login,
- class: "w-full bg-purple-600 text-white font-semibold py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors"
- }, "Connect with Nostr"),
-}
-
-const AlertStatus: m.Component<{alert: Alert}> = {
- view: vnode => {
- const {alert} = vnode.attrs
- const {alertStatuses} = state.get()
- const address = getAddress(alert.event)
- const alertStatus = alertStatuses.find(s => getTagValue('d', s.event.tags) === address)
- const status = getTagValue('status', alertStatus?.tags || [])
- const message = getTagValue('message', alertStatus?.tags || [])
-
- const getStatusClasses = () => {
- const baseClasses = "rounded-full px-3 py-1 text-sm border"
- if (status === 'ok') return `${baseClasses} border-green-500 text-green-500`
- if (status === 'pending') return `${baseClasses} border-yellow-500 text-yellow-500`
- return `${baseClasses} border-red-500 text-red-500`
- }
-
- const getStatusDisplay = () => {
- if (!status) return 'Inactive'
- if (status === 'ok') return 'Active'
- if (status === 'pending') return 'Pending'
- return status.replace('-', ' ').replace(/^(.)/, x => x.toUpperCase())
- }
-
- return m("div", {class: getStatusClasses(), tooltip: message}, getStatusDisplay())
- },
-}
-
-const AlertListItem: m.Component<{alert: Alert}> = {
- view: vnode => {
- const {alert} = vnode.attrs
- const cron = getTagValue('cron', alert.tags)
- const feeds = getTagValues('feed', alert.tags)
- const channel = getTagValue('channel', alert.tags)
- const description = displayFeeds(feeds.map(feed => parseJson(feed))) || "[invalid feed]"
-
- let frequency = cron || "Unknown"
- if (cron) {
- if (CRON_DAILY_PATTERN.test(cron)) {
- frequency = 'Daily'
- } else if (CRON_WEEKLY_PATTERN.test(cron)) {
- frequency = 'Weekly'
- }
- }
-
- return m("div", { class: "flex items-start justify-between p-4" }, [
- m("button", {
- onclick: () => deleteAlert(alert),
- class: "mr-4 mt-1",
- tooltip: "Delete alert"
- }, [m.trust(TRASH_ICON)]),
- m("div", { class: "space-y-2 flex-grow" }, [
- m("div", { class: "text-gray-600" }, `${frequency} alert via ${channel}`),
- m("div", { class: "text-sm text-gray-500" }, `Events ${description}`)
- ]),
- m(AlertStatus, {alert}),
- ])
- }
-}
-
-const AlertList = {
- oninit: loadAlerts,
- view: () => {
- const {alerts, alertsLoading} = state.get()
-
- const content = alertsLoading
- ? m(Loader)
- : alerts.length > 0
- ? alerts.map(alert => m(AlertListItem, {alert, key: alert.event.id}))
- : m("div", { class: "text-center text-gray-500 py-8" }, [
- "You don't have any alerts set up.",
- ])
-
- return m("div", { class: "space-y-4" }, [
- m("div", { class: "flex items-center justify-between mb-6" }, [
- m("h1", { class: "text-2xl font-bold text-gray-900" }, "Your Nostr Alerts"),
- m("a", {
- href: "#!/alerts/new",
- class: "flex items-center gap-2 bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors",
- }, [
- m.trust(PLUS_ICON),
- "Add Alert"
- ])
- ]),
- m("div", { class: "bg-white shadow rounded-lg p-6" }, content)
- ])
- }
-}
-
-const AlertCreate = {
- oninit: () => {
- state.update(assoc('alertDraft', {
- email: getTagValue('email', state.get().alerts[0]?.tags || []) || "",
- freq: 'daily',
- time: '17:00',
- feedAddress: "",
- secret: "",
- }))
- },
- view: () => {
- const {pubkey, alertDraft, alertsLoading} = state.get()
- const {email, feedAddress, freq, time, secret} = alertDraft!
-
- const update = (newValues: Partial) => {
- state.update(assoc('alertDraft', {...alertDraft, ...newValues}))
- }
-
- const submit = async (e: Event) => {
- e.preventDefault()
-
- state.update(assoc('alertsLoading', true))
-
- try {
- if (!email.includes("@")) return alert("Please provide a valid email address")
-
- const address = tryCatch(() => Address.fromNaddr(fromNostrURI(feedAddress)))
-
- if (!address) return alert("Please provide a valid feed address")
- if (address.kind !== FEED) return alert(`Please provide a valid feed address (kind ${FEED})`)
-
- const selections = await load({
- relays: INDEXER_RELAYS,
- filters: [{kinds: [RELAYS], authors: [pubkey!, address.pubkey]}],
- })
-
- const router = Router.get()
- const filters = getIdFilters([address.toString()])
- const scenario = router.merge([
- router.FromRelays(selections.flatMap(e => getRelaysFromList(readList(asDecryptedEvent(e)), RelayMode.Write))),
- router.FromRelays(address.relays),
- router.FromRelays(INDEXER_RELAYS),
- ])
- const relays = scenario.limit(10).getUrls()
-
- const [event] = await load({relays, filters})
-
- if (!event) return alert("Sorry, we weren't able to find that feed")
-
- const feedStrings = getTagValues('feed', event.tags)
-
- if (feedStrings.length === 0) return alert('At least one feed is required')
-
- const feeds = removeNil(feedStrings.map(parseJson))
-
- if (feeds.length < feedStrings.length) return alert("At least one feed is invalid (must be valid JSON)")
-
- const feedError = feeds.map(validateFeed).find(e => e instanceof ValidationError)
-
- if (feedError) return alert(`At least one feed is invalid (${feedError.data.toLowerCase()}).`)
-
- await publishAlert({freq, time, email, feeds, secret})
-
- m.route.set("/alerts")
- } catch (error) {
- alert("Failed to create alert. Please try again.")
- console.error('Error creating alert:', error)
- } finally {
- state.update(assoc('alertsLoading', false))
- }
- }
-
- return m("div", { class: "space-y-4" }, [
- m("div", { class: "flex items-center gap-4 mb-6" }, [
- m("button", {
- onclick: () => m.route.set("/alerts"),
- class: "text-gray-600 hover:text-gray-900 cursor-pointer",
- tooltip: "Back to alerts"
- }, m.trust(ARROW_LEFT_ICON)),
- m("h1", { class: "text-2xl font-bold text-gray-900" }, "Create Alert")
- ]),
- m("div", { class: "bg-white shadow rounded-lg p-6" }, [
- m("form", { class: "space-y-6", onsubmit: submit }, [
- m("div", [
- m("label", { class: "block text-sm font-medium text-gray-700 mb-1" }, "Email"),
- m("input", {
- type: "email",
- placeholder: "Enter your email address",
- value: email,
- oninput: (e: InputEvent) => update({email: (e.target as HTMLInputElement).value}),
- class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500"
- })
- ]),
- m("div", {class: "w-full flex gap-2"}, [
- m("div", {class: "flex-grow"}, [
- m("label", { class: "block text-sm font-medium text-gray-700 mb-1" }, "Frequency"),
- m("select", {
- value: freq,
- onchange: (e: Event) => update({freq: (e.target as HTMLSelectElement).value}),
- class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500"
- }, [
- m("option", { value: 'daily' }, "Daily"),
- m("option", { value: '0' }, "Weekly on Sunday"),
- m("option", { value: '1' }, "Weekly on Monday"),
- m("option", { value: '2' }, "Weekly on Tuesday"),
- m("option", { value: '3' }, "Weekly on Wednesday"),
- m("option", { value: '4' }, "Weekly on Thursday"),
- m("option", { value: '5' }, "Weekly on Friday"),
- m("option", { value: '6' }, "Weekly on Saturday"),
- ])
- ]),
- m("div", [
- m("label", { class: "block text-sm font-medium text-gray-700 mb-1" }, "Time"),
- m("input", {
- value: time,
- onchange: (e: Event) => update({time: (e.target as HTMLSelectElement).value}),
- type: "time",
- class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500"
- })
- ]),
- ]),
- m("div", [
- m("label", { class: "block text-sm font-medium text-gray-700 mb-1" }, "Feed Address"),
- m("div", { class: "space-y-2" }, [
- m("input", {
- type: "text",
- placeholder: "naddr1...",
- value: feedAddress,
- oninput: (e: InputEvent) => update({feedAddress: (e.target as HTMLInputElement).value}),
- class: "w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-purple-500 focus:border-purple-500"
- }),
- m("p", { class: "text-sm text-gray-500" }, [
- "Visit ",
- m("a", {
- href: "https://coracle.social/feeds",
- target: "_blank",
- class: "text-purple-600 hover:text-purple-800"
- }, "coracle.social/feeds"),
- " to search for existing feeds or create a new one. Copy the feed address (starts with 'naddr1') and paste it here."
- ])
- ])
- ]),
- m("div", { class: "flex justify-end" }, [
- m("button", {
- type: "submit",
- disabled: alertsLoading,
- class: "bg-purple-600 text-white px-4 py-2 rounded-lg hover:bg-purple-700 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
- }, alertsLoading ? "Creating..." : "Create Alert")
- ])
- ])
- ])
- ])
- }
-}
-
-const FailedToLogin = {
- view: () =>
- m("div", { class: "space-y-6 text-center" }, [
- m("div", { class: "bg-red-50 border border-red-200 rounded-lg p-6" }, [
- m("h2", { class: "text-red-800 font-semibold mb-2" }, "Unable to Connect"),
- m("p", { class: "text-red-600 mb-4" }, "To use Anchor Alerts, you need a Nostr signer extension installed in your browser."),
- m("div", { class: "space-y-3" }, [
- m("button", {
- onclick: () => window.location.reload(),
- class: "w-full bg-red-100 text-red-700 font-medium py-2 px-4 rounded-lg hover:bg-red-200 transition-colors"
- }, "Try Again"),
- m("a", {
- href: "https://nostrapps.com/#signers",
- target: "_blank",
- class: "block w-full bg-purple-600 text-white font-medium py-2 px-4 rounded-lg hover:bg-purple-700 transition-colors"
- }, "Install a Nostr Signer")
- ])
- ])
- ])
-}
-
-const Layout: m.Component<{children: m.Children}> = {
- view: vnode => {
- const {children} = vnode.attrs
- const {failedToLogin, pubkey} = state.get()
-
- if (failedToLogin) {
- return m(FailedToLogin)
- }
-
- if (!pubkey) {
- return m("div", { class: "text-center space-y-4" }, [
- m("h1", { class: "text-2xl font-bold text-gray-900 mb-2" }, "Welcome to Anchor Alerts"),
- m("p", { class: "text-gray-600 mb-6" }, "Connect your Nostr signer to get started"),
- m(Login)
- ])
- }
-
- return children
- }
-}
-
-m.route(document.querySelector('#app')!, "/alerts", {
- "/alerts": {
- view: () => {
- return m(Layout, {children: [m(AlertList)]})
- },
- },
- "/alerts/new": {
- view: () => {
- return m(Layout, {children: [m(AlertCreate)]})
- }
- },
-})
-
-state.subscribe(() => m.redraw())
-
-defaultSocketPolicies.push(
- makeSocketPolicyAuth({
- sign: (event: StampedEvent) => {
- return state.get().signer?.sign(event)
- },
- }),
-)
-
-Object.assign(window, {setJson, getJson})
diff --git a/web/src/style.css b/web/src/style.css
deleted file mode 100644
index 1c1b184..0000000
--- a/web/src/style.css
+++ /dev/null
@@ -1,42 +0,0 @@
-@import "tailwindcss";
-
-a, button {
- @apply cursor-pointer;
-}
-
-/* Tooltip styling */
-[tooltip] {
- @apply cursor-pointer relative;
-}
-
-[tooltip]:hover::after {
- content: attr(tooltip);
- position: absolute;
- bottom: 100%;
- left: 50%;
- transform: translateX(-50%);
- padding: 4px 8px;
- background-color: rgba(0, 0, 0, 0.8);
- color: white;
- border-radius: 4px;
- font-size: 14px;
- white-space: nowrap;
- z-index: 1000;
- pointer-events: none;
-
- /* Animation properties */
- opacity: 0;
- animation: tooltipFadeIn 0.2s ease-in-out forwards;
-}
-
-/* Keyframes for fade in animation */
-@keyframes tooltipFadeIn {
- from {
- opacity: 0;
- transform: translateX(-50%) translateY(0);
- }
- to {
- opacity: 1;
- transform: translateX(-50%) translateY(-3px);
- }
-}
diff --git a/web/src/vite-env.d.ts b/web/src/vite-env.d.ts
deleted file mode 100644
index 11f02fe..0000000
--- a/web/src/vite-env.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-///
diff --git a/web/tsconfig.json b/web/tsconfig.json
deleted file mode 100644
index 9d2104f..0000000
--- a/web/tsconfig.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "compilerOptions": {
- "target": "ES2020",
- "useDefineForClassFields": true,
- "module": "ESNext",
- "lib": ["ESNext", "DOM", "DOM.Iterable"],
- "skipLibCheck": true,
-
- /* Bundler mode */
- "moduleResolution": "bundler",
- "allowImportingTsExtensions": true,
- "isolatedModules": true,
- "moduleDetection": "force",
- "noEmit": true,
-
- /* Linting */
- "strict": true,
- "noFallthroughCasesInSwitch": true,
- "noUncheckedSideEffectImports": true
- },
- "include": ["src"]
-}
diff --git a/web/vite.config.js b/web/vite.config.js
deleted file mode 100644
index bd48de3..0000000
--- a/web/vite.config.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import { defineConfig } from 'vite'
-import tailwindcss from '@tailwindcss/vite'
-
-export default defineConfig({
- server: {
- port: 2893,
- },
- plugins: [
- tailwindcss(),
- ],
-})