diff --git a/apps/bootstrapper/.env.example b/apps/bootstrapper/.env.example new file mode 100644 index 00000000..7abac93c --- /dev/null +++ b/apps/bootstrapper/.env.example @@ -0,0 +1,15 @@ +# Environment Configuration +NODE_ENV=development + +# Database +DATABASE_URL=./data/bootstrapper.db + +# Nostr Configuration +NOSTR_BOOTSTRAPPER_PRIVATE_KEY= + +# Server Configuration +PORT=8001 +HOST=0.0.0.0 + +# Logging +LOG_LEVEL=info diff --git a/apps/bootstrapper/.gitignore b/apps/bootstrapper/.gitignore new file mode 100644 index 00000000..7b0c46c6 --- /dev/null +++ b/apps/bootstrapper/.gitignore @@ -0,0 +1,92 @@ +# Dependencies +node_modules/ + +# SvelteKit build outputs +build/ +dist/ +.svelte-kit/ + +# Temporary folders +tmp/ +temp/ + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Environment files +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Database +*.db +*.sqlite +*.sqlite3 +data/ + +# IDE and editor files +.vscode/ +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.swp +*.swo +*~ + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# Coverage directory used by tools like istanbul +coverage/ +*.lcov + +# TypeScript cache +*.tsbuildinfo + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# Yarn +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* + +# Vercel +.vercel + +# Playwright +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + +# Vitest +coverage/ + +# Custom application files +/static/uploads/ +*.key +*.crt +*.pem diff --git a/apps/bootstrapper/.prettierrc b/apps/bootstrapper/.prettierrc new file mode 100644 index 00000000..7e0dcde7 --- /dev/null +++ b/apps/bootstrapper/.prettierrc @@ -0,0 +1,24 @@ +{ + "useTabs": false, + "tabWidth": 4, + "singleQuote": true, + "semi": true, + "trailingComma": "es5", + "printWidth": 100, + "plugins": ["prettier-plugin-svelte"], + "pluginSearchDirs": ["."], + "overrides": [ + { + "files": "*.svelte", + "options": { + "parser": "svelte", + "useTabs": false, + "tabWidth": 4, + "singleQuote": true, + "semi": true, + "trailingComma": "es5", + "printWidth": 100 + } + } + ] +} diff --git a/apps/bootstrapper/app.postcss b/apps/bootstrapper/app.postcss new file mode 100644 index 00000000..b5c61c95 --- /dev/null +++ b/apps/bootstrapper/app.postcss @@ -0,0 +1,3 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; diff --git a/apps/bootstrapper/package.json b/apps/bootstrapper/package.json new file mode 100644 index 00000000..9a87a9f2 --- /dev/null +++ b/apps/bootstrapper/package.json @@ -0,0 +1,47 @@ +{ + "name": "satshoot-bootstrapper", + "version": "0.1.0", + "type": "module", + "scripts": { + "dev": "vite dev --host 0.0.0.0 --port 8001", + "build": "vite build", + "preview": "vite preview --port 8001", + "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json", + "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch", + "test": "vitest", + "test:e2e": "playwright test", + "lint": "eslint .", + "format": "prettier --write ." + }, + "devDependencies": { + "@playwright/test": "^1.47.0", + "@skeletonlabs/skeleton": "^3.1.2", + "@skeletonlabs/skeleton-svelte": "^1.2.1", + "@sveltejs/adapter-node": "^5.2.7", + "@sveltejs/kit": "^2.20.7", + "@sveltejs/vite-plugin-svelte": "^5.0.3", + "@tailwindcss/forms": "^0.5.10", + "@tailwindcss/typography": "^0.5.16", + "@tailwindcss/vite": "^4.1.4", + "@types/node": "^20.17.30", + "@types/ws": "^8.5.12", + "eslint": "^9.13.0", + "prettier": "^3.6.2", + "prettier-plugin-svelte": "^3.2.6", + "svelte": "^5.27.1", + "svelte-check": "^4.1.6", + "tailwindcss": "^4.1.4", + "tslib": "^2.8.1", + "typescript": "^5.8.3", + "vite": "^6.3.1", + "vitest": "^3.1.1" + }, + "dependencies": { + "@nostr-dev-kit/ndk": "workspace:^", + "@nostr-dev-kit/ndk-svelte": "workspace:^", + "@sveltejs/adapter-static": "^3.0.8", + "nostr-tools": "~2.5.2", + "svelte-persisted-store": "^0.11.0", + "ws": "^8.18.0" + } +} diff --git a/apps/bootstrapper/src/app.css b/apps/bootstrapper/src/app.css new file mode 100644 index 00000000..7aa60aa3 --- /dev/null +++ b/apps/bootstrapper/src/app.css @@ -0,0 +1,11 @@ +/* Base styles */ +html, +body { + @apply h-full overflow-hidden; +} + +.app { + display: flex; + flex-direction: column; + min-height: 100vh; +} diff --git a/apps/bootstrapper/src/app.d.ts b/apps/bootstrapper/src/app.d.ts new file mode 100644 index 00000000..06e03f2e --- /dev/null +++ b/apps/bootstrapper/src/app.d.ts @@ -0,0 +1,14 @@ +// See https://kit.svelte.dev/docs/types#app + +// for information about these interfaces +declare global { + namespace App { + // interface Error {} + // interface Locals {} + // interface PageData {} + // interface PageState {} + // interface Platform {} + } +} + +export {}; diff --git a/apps/bootstrapper/src/app.html b/apps/bootstrapper/src/app.html new file mode 100644 index 00000000..1141bc4e --- /dev/null +++ b/apps/bootstrapper/src/app.html @@ -0,0 +1,13 @@ + + + + + + + SatShoot Bootstrapper + %sveltekit.head% + + +
%sveltekit.body%
+ + diff --git a/apps/bootstrapper/src/app.js b/apps/bootstrapper/src/app.js new file mode 100644 index 00000000..7ef27151 --- /dev/null +++ b/apps/bootstrapper/src/app.js @@ -0,0 +1,2 @@ +import '@tailwindcss/browser'; +import './app.css'; diff --git a/apps/bootstrapper/src/routes/+layout.svelte b/apps/bootstrapper/src/routes/+layout.svelte new file mode 100644 index 00000000..d310ef4f --- /dev/null +++ b/apps/bootstrapper/src/routes/+layout.svelte @@ -0,0 +1,5 @@ + + + diff --git a/apps/bootstrapper/src/routes/+page.svelte b/apps/bootstrapper/src/routes/+page.svelte new file mode 100644 index 00000000..23a72cab --- /dev/null +++ b/apps/bootstrapper/src/routes/+page.svelte @@ -0,0 +1,234 @@ +
+ +
+
+
+ +
+ +
+
+ 🚀 +
+

+ Bootstrapper Account Service +

+

+ Advanced Nostr Job Management with Intelligent Bid Resolution +

+
+ + +
+
+
0
+
Active Jobs
+
+
+
0
+
Pending Bids
+
+
+
0
+
Completed
+
+
+
0
+
Providers
+
+
+ + +
+ +
+
+
+
+ 🔗 +
+

Connection Status

+
+ +
+
+
+
+ Local Relay +
+ Disconnected +
+ +
+
+
+ Database +
+ Not initialized +
+ +
+
+
+ WebSocket +
+ Waiting +
+
+
+
+ + +
+
+
+
+ +
+

Quick Actions

+
+ +
+ + + + + + + +
+
+
+ + +
+
+
+
+ ⚙️ +
+

Configuration

+
+ +
+
+ Bid Timeout + 5 minutes +
+ +
+ Auto Resolution + +
+ Enabled +
+
+ +
+ Min Reputation + 0 +
+ +
+ Active Relays + 1 +
+
+
+
+
+ + +
+
+
+ + Bootstrapper Account Service v0.1.0 • Running on localhost:8001 + +
+ +

+ Built with SvelteKit & Tailwind CSS • Powered by Nostr Protocol +

+
+
+
diff --git a/apps/bootstrapper/svelte.config.js b/apps/bootstrapper/svelte.config.js new file mode 100644 index 00000000..e8852be0 --- /dev/null +++ b/apps/bootstrapper/svelte.config.js @@ -0,0 +1,46 @@ +import adapter from '@sveltejs/adapter-static'; +import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; + +/** @type {import('@sveltejs/kit').Config} */ +const config = { + // Consult https://kit.svelte.dev/docs/integrations#preprocessors + // for more information about preprocessors + // This is needed for vite to handle Typescript in svelte components + preprocess: vitePreprocess({ script: true }), + + vite: { + server: { + fs: { + allow: ['./packages/ndk'], + }, + }, + }, + + kit: { + version: { + // Every 10 secs this checks if app was updated + // updated means build timestamp checking by default + // see: https://kit.svelte.dev/docs/configuration#version + // pollInterval: 10000 + }, + adapter: adapter( + // ---------------- For build ------------------ + { + pages: 'build/htdocs', + assets: 'build/htdocs', + // For SPA this is important. If a dynamic route is requested on a static site, + // a fallback page is the response which svelte recognizes on the client-side + // and tries to do client-side dynamic routing. Hosting provider specific option. + + // For test deploy use commented fallback page + // fallback: 'index.html', + fallback: 'index.html', + precompress: false, + // strict is needed to check if all sites have prerender = true OR have a fallback page(see above) + strict: true, + } + ), + }, +}; + +export default config; diff --git a/apps/bootstrapper/tsconfig.json b/apps/bootstrapper/tsconfig.json new file mode 100644 index 00000000..834d87d9 --- /dev/null +++ b/apps/bootstrapper/tsconfig.json @@ -0,0 +1,20 @@ +{ + "extends": "./.svelte-kit/tsconfig.json", + "compilerOptions": { + "allowJs": true, + "checkJs": true, + "lib": ["dom", "dom.iterable", "esnext"], + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "bundler", + "types": ["vitest/globals"] + } + // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias + // + // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes + // from the referenced tsconfig.json - TypeScript does not merge them in +} diff --git a/apps/bootstrapper/vite.config.ts b/apps/bootstrapper/vite.config.ts new file mode 100644 index 00000000..6e10f833 --- /dev/null +++ b/apps/bootstrapper/vite.config.ts @@ -0,0 +1,18 @@ +import { sveltekit } from '@sveltejs/kit/vite'; +import { defineConfig } from 'vite'; +import tailwindcss from '@tailwindcss/vite'; + +export default defineConfig({ + plugins: [sveltekit(), tailwindcss()], + server: { + port: 8001, + host: '0.0.0.0', + }, + preview: { + port: 8001, + host: '0.0.0.0', + }, + test: { + include: ['src/**/*.{test,spec}.{js,ts}'], + }, +}); diff --git a/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte b/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte new file mode 100644 index 00000000..5d37cfe5 --- /dev/null +++ b/apps/satshoot/src/lib/components/Onboarding/PendingOnboardingJobs.svelte @@ -0,0 +1,58 @@ + + +
+

⭐ Onboarding ⭐

+

Complete the following jobs to earn rewards on SatShoot:

+ {#each Array.from(jobList) as job (job.id)} + + {/each} +
diff --git a/apps/satshoot/src/lib/events/OnboardingJobEvent.ts b/apps/satshoot/src/lib/events/OnboardingJobEvent.ts new file mode 100644 index 00000000..46ac9ece --- /dev/null +++ b/apps/satshoot/src/lib/events/OnboardingJobEvent.ts @@ -0,0 +1,84 @@ +import { NDKEvent, type NDKTag, type NostrEvent } from '@nostr-dev-kit/ndk'; +import NDK from '@nostr-dev-kit/ndk'; +import { NDKKind } from '@nostr-dev-kit/ndk'; + +export enum OnboardingJobStatus { + New = 0, + InProgress = 1, + Resolved = 2, + Failed = 3, +} + +export class OnboardingJobEvent extends NDKEvent { + private _status: OnboardingJobStatus; + private _title: string; + + constructor(ndk?: NDK, rawEvent?: NostrEvent) { + super(ndk, rawEvent); + this.kind ??= NDKKind.FreelanceJob; + this._status = parseInt(this.tagValue('s') as string); + this._title = this.tagValue('title') as string; + } + + static from(event: NDKEvent) { + return new OnboardingJobEvent(event.ndk, event.rawEvent()); + } + + get jobAddress(): string { + return this.tagAddress(); + } + + // this.generateTags() will take care of setting d-tag + + get acceptedBidAddress(): string | undefined { + return this.tagValue('a'); + } + + set acceptedBidAddress(bidAddress: string) { + // Can only have exactly one accepted bid tag + this.removeTag('a'); + this.tags.push(['a', bidAddress]); + this.status = OnboardingJobStatus.InProgress; + } + + get winnerFreelancer(): string | undefined { + return this.acceptedBidAddress?.split(':')[1]; + } + + get title(): string { + return this._title; + } + + set title(title: string) { + this._title = title; + // Can only have exactly one title tag + this.removeTag('title'); + this.tags.push(['title', title]); + } + + get status(): OnboardingJobStatus { + return this._status; + } + + set status(status: OnboardingJobStatus) { + this._status = status; + this.removeTag('s'); + this.tags.push(['s', status.toString()]); + } + + public isClosed(): boolean { + return this._status === OnboardingJobStatus.Resolved || this._status === OnboardingJobStatus.Failed; + } + + get description(): string { + return this.content; + } + + set description(desc: string) { + this.content = desc; + } + + get tTags(): NDKTag[] { + return this.tags.filter((tag: NDKTag) => tag[0] === 't'); + } +} diff --git a/apps/satshoot/src/lib/stores/freelance-eventstores.ts b/apps/satshoot/src/lib/stores/freelance-eventstores.ts index 60659609..f6ae34ac 100644 --- a/apps/satshoot/src/lib/stores/freelance-eventstores.ts +++ b/apps/satshoot/src/lib/stores/freelance-eventstores.ts @@ -6,6 +6,7 @@ import { NDKKind } from '@nostr-dev-kit/ndk'; import { JobEvent } from '$lib/events/JobEvent'; import { BidEvent } from '$lib/events/BidEvent'; +import { OnboardingJobEvent } from '$lib/events/OnboardingJobEvent'; import { wot } from '$lib/stores/wot'; @@ -56,6 +57,11 @@ export const myOrderFilter: NDKFilter = { kinds: [NDKKind.FreelanceOrder], }; +// The filter's pubkey part will be filled in when user logs in +export const myOnboardingJobFilter: NDKFilter = { + kinds: [NDKKind.FreelanceJob], +}; + export const allJobs: NDKEventStore> = get(ndk).storeSubscribe( allJobsFilter, subOptions, @@ -111,3 +117,7 @@ export const myServices: NDKEventStore> = get( export const myOrders: NDKEventStore> = get( ndk ).storeSubscribe(myOrderFilter, subOptions, OrderEvent); + +export const myOnboardingJobs: NDKEventStore> = get( + ndk +).storeSubscribe(myOnboardingJobFilter, subOptions, OnboardingJobEvent); diff --git a/apps/satshoot/src/routes/[npub=user]/+page.svelte b/apps/satshoot/src/routes/[npub=user]/+page.svelte index 741179c3..8cb4e355 100644 --- a/apps/satshoot/src/routes/[npub=user]/+page.svelte +++ b/apps/satshoot/src/routes/[npub=user]/+page.svelte @@ -17,6 +17,7 @@ import { debounce } from '$lib/utils/misc'; import ServicesAndBids from '$lib/components/ProfilePage/ServicesAndBids.svelte'; import OrdersAndJobs from '$lib/components/ProfilePage/OrdersAndJobs.svelte'; + import PendingOnboardingJobs from '$lib/components/Onboarding/PendingOnboardingJobs.svelte'; import { BidEvent } from '$lib/events/BidEvent'; import { JobEvent } from '$lib/events/JobEvent'; import { ServiceEvent } from '$lib/events/ServiceEvent'; @@ -349,6 +350,7 @@ bind:this={eventContainerElement} >
+ {#if componentOrder && componentOrder.first === 'ServicesAndBids'}