diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..046f296 --- /dev/null +++ b/.env.example @@ -0,0 +1,15 @@ +# Earth Walker Design — site env vars +# +# Both are inlined at build time by Vite. To rotate either, edit +# values here, then rebuild + redeploy. + +# The Nostr public key that receives encrypted inquiry submissions. +# Bech32 npub1... form. Generate via `nak key generate` (fiatjaf/nak) +# or any Nostr client. +VITE_OWNER_NPUB= + +# Optional. Comma-separated wss:// relay URLs the inquiry form +# publishes to. If unset, defaults to: +# wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band +# The submission succeeds if at least one relay accepts the event. +VITE_NOSTR_RELAYS= diff --git a/README.md b/README.md index 977732b..defaaef 100644 --- a/README.md +++ b/README.md @@ -1,74 +1,122 @@ -# boilerplate-website +# Earth Walker Design -Opinionated Vue 3 starter — the base every new aiolabs website forks from. +Studio site for an interior designer. Vue 3 + Vite + shadcn-vue + +Tailwind 4. Anonymous-by-default contact: inquiry submissions are +encrypted in the visitor's browser and delivered to the designer's +Nostr inbox. -## Stack +## Routes -- **Vue 3.5** + **Vite 8** + **TypeScript 6** -- **shadcn-vue** (via `reka-ui` + `class-variance-authority` + `tailwind-merge`) — components.json pre-wired, run `pnpm dlx shadcn-vue@latest add ` to copy components in -- **Tailwind CSS 4** (via `@tailwindcss/vite` — no `tailwind.config.js`) -- **Pinia 3** — sample `useCounterStore` in `src/stores/` -- **Vue Router 5** — file in `src/router/index.ts`, lazy-loaded views in `src/views/` -- **Vue I18n 11** — `src/i18n/locales/{en,es}.json` -- **vee-validate 4** + **zod 3** — form validation primitives (zod pinned to ^3 until `@vee-validate/zod` ships a v4-compatible resolver) -- **@lucide/vue** — icon set (`lucide-vue-next` is deprecated upstream) -- **ESLint 10** (flat config) + **Prettier 3** +| Path | Purpose | +|---|---| +| `/` | Hero, studio voice, two project teasers, inquiry CTA | +| `/portfolio` | Two-up project grid (Boulder, Asheville) | +| `/portfolio/boulder` | Boulder project detail — editorial image scroll + lightbox | +| `/portfolio/asheville` | Asheville project detail | +| `/contact` | Inquiry form (Nostr-delivered) | -## Quick start +## Local dev ```sh pnpm install -pnpm dev # vite dev server -pnpm build # type-check + production build -pnpm preview # serve dist/ +cp .env.example .env # fill in VITE_OWNER_NPUB +pnpm dev # http://localhost:5173 +pnpm build # type-check + production build to dist/ +pnpm preview # serve dist/ pnpm lint pnpm format ``` -## Layout +The contact form requires `VITE_OWNER_NPUB` to be set — without it, +`submitInquiry` throws so visitors see an explicit error rather +than silently dropping submissions. Generate one with +[`nak key generate`](https://github.com/fiatjaf/nak) or any Nostr client. -``` -src/ -├─ App.vue # router-view shell -├─ main.ts # plugin wiring -├─ style.css # tailwind + shadcn-vue CSS variables -├─ lib/utils.ts # cn() — shadcn-vue's class merger -├─ router/index.ts -├─ stores/counter.ts # example pinia store -├─ i18n/ -│ ├─ index.ts -│ └─ locales/{en,es}.json -├─ views/HomeView.vue # proof-of-wiring page (i18n + pinia + tailwind) -└─ features/ - ├─ nostr/README.md # opt-in: nostr-tools wiring (see file) - └─ lnbits/README.md # opt-in: LNbits payments wiring (see file) -``` +## Env vars -## Optional features +| Var | Required | Default | Notes | +|---|---|---|---| +| `VITE_OWNER_NPUB` | yes | — | The designer's Nostr public key (`npub1…`). Inquiries are NIP-17 gift-wrapped to this key. | +| `VITE_NOSTR_RELAYS` | no | `wss://relay.damus.io,wss://nos.lol,wss://relay.nostr.band` | Comma-separated wss:// list. Submission succeeds if any one relay accepts. | -Both nostr and LNbits live as **documentation-only** folders. The deps -aren't installed by default — bundle stays small for sites that don't -need them. Each folder's README walks you through enabling. +Both are inlined at build time by Vite. Rotating either requires a +rebuild + redeploy. -- **`src/features/nostr/`** — connect to relays, sign/publish events, contact forms that DM the site owner's npub -- **`src/features/lnbits/`** — create invoices, accept Lightning payments via an LNbits instance +## How the contact form delivers inquiries -## Versioning strategy +1. Visitor fills the form (name optional, contact method + value, + message). Validation runs client-side via `vee-validate` + `zod`. +2. `submitInquiry()` (`src/features/nostr/submitInquiry.ts`): + - generates a fresh ephemeral secp256k1 keypair + - decodes `VITE_OWNER_NPUB` to hex + - calls `nip17.wrapEvent()` to produce a kind:1059 gift-wrap with + NIP-44 v2 encryption inside (the visitor's identity is the + throwaway key, not anything they entered) + - publishes via `SimplePool` to the configured relays in parallel +3. The designer receives the inquiry as a DM in any NIP-17 capable + Nostr client (Damus, Amethyst, 0xchat, etc.) signed in with the + matching nsec. -The boilerplate's `main` branch is **the "tools wired, no content" baseline**. New site = clone (or fork on Forgejo) → start adding content immediately. +No server in between stores the message. There is no inbox to leak. -To pull dep refreshes from the boilerplate into an existing site: +## Theming + +Light + dark stone-warm palettes live in `src/style.css` as OKLCH +CSS variables. Toggle persists to `localStorage` under +`ewd:theme` (see `src/composables/useTheme.ts`). Typography pairs +Fraunces (display, h1–h3, brand wordmark) with Inter (UI/body) via +Bunny Fonts, declared in `index.html` and bound through Tailwind 4 +`@theme` directives. + +To recolor: edit the OKLCH triples in `src/style.css` under `:root` +and `.dark`. Keep all shadcn-vue token names intact. + +## Image discipline + +All images live under `public/images//`. The build pipeline +expects: + +- Filenames are sequence-numbered (`01.jpg`, `02.jpg`, …) — never + leak addresses, neighborhood names, or owner identities in the + filename. +- EXIF / GPS / timestamp / maker metadata is stripped before commit. + Re-run on new assets: + ```sh + nix-shell -p imagemagick --run \ + 'for f in *.jpg; do magick "$f" -auto-orient -strip -resize "2400x2400>" \ + -interlace Plane -sampling-factor 4:2:0 -quality 82 "${f}.tmp" \ + && mv "${f}.tmp" "$f"; done' + ``` +- Alt text in `src/data/projects.ts` describes the room/feature, not + the location. + +## Adding a project + +1. Drop sequence-numbered images into `public/images//`. +2. Add a new `Project` entry in `src/data/projects.ts` (typed) with + `slug`, `name`, `eyebrow`, `intro`, `cover`, `coverAlt`, and an + `images[]` array. Each image takes a `feature` tag + (`hero` | `wide` | `narrow` | `paired`) that drives the layout + slot in `ProjectDetail.vue`'s editorial scroll. +3. Add a 4-line view at `src/views/projects/View.vue`: + ```vue + + + ``` +4. Register the route in `src/router/index.ts`. +5. The `PortfolioView` already iterates `projects[]`, so the new + project's card appears automatically. + +## Stack + +Tracking the upstream boilerplate (`aiolabs/boilerplate-website`). +To pull dependency refreshes: ```sh git remote add boilerplate forgejo@git.atitlan.io:aiolabs/boilerplate-website.git git fetch boilerplate git merge boilerplate/main ``` - -When a site diverges enough to no longer benefit from these merges, drop the remote — it becomes its own thing. - -## Keeping deps current - -- The boilerplate gets dep bumps via PRs (Renovate / Dependabot configurable; otherwise periodic `pnpm update --latest` + manual review). -- The Vue ecosystem (vue, pinia, router, i18n, vee-validate) versions in lockstep — when one majors, the others usually follow within weeks. -- Tailwind 4 + shadcn-vue + reka-ui is the current modern combo. shadcn-vue uses tw-animate-css (not the deprecated tailwindcss-animate plugin). diff --git a/env.d.ts b/env.d.ts index 11f02fe..ad3afc1 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1 +1,12 @@ /// + +interface ImportMetaEnv { + /** Hex- or bech32-encoded npub of the inquiry recipient. */ + readonly VITE_OWNER_NPUB?: string + /** Comma-separated wss:// relay list; falls back to a default set if unset. */ + readonly VITE_NOSTR_RELAYS?: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} diff --git a/package.json b/package.json index 8c678b7..1486224 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@vueuse/core": "^14.3.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "nostr-tools": "^2.23.5", "pinia": "^3.0.4", "reka-ui": "^2.9.8", "tailwind-merge": "^3.6.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48b159c..f90a373 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 + nostr-tools: + specifier: ^2.23.5 + version: 2.23.5(typescript@6.0.3) pinia: specifier: ^3.0.4 version: 3.0.4(typescript@6.0.3)(vue@3.5.34(typescript@6.0.3)) @@ -257,6 +260,18 @@ packages: '@emnapi/core': ^1.7.1 '@emnapi/runtime': ^1.7.1 + '@noble/ciphers@2.1.1': + resolution: {integrity: sha512-bysYuiVfhxNJuldNXlFEitTVdNnYUc+XNJZd7Qm2a5j1vZHgY+fazadNFWFaMK/2vye0JVlxV3gHmC0WDfAOQw==} + engines: {node: '>= 20.19.0'} + + '@noble/curves@2.0.1': + resolution: {integrity: sha512-vs1Az2OOTBiP4q0pwjW5aF0xp9n4MxVrmkFBxc6EKZc6ddYx5gaZiAsZoq0uRRXWbi3AT/sBqn05eRPtn1JCPw==} + engines: {node: '>= 20.19.0'} + + '@noble/hashes@2.0.1': + resolution: {integrity: sha512-XlOlEbQcE9fmuXxrVTXCTlG2nlRXa9Rj3rr5Ue/+tX+nmkgbX720YHh0VR3hBF9xDvwnb8D2shVGOwNx+ulArw==} + engines: {node: '>= 20.19.0'} + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -374,6 +389,15 @@ packages: '@rolldown/pluginutils@1.0.1': resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@scure/base@2.0.0': + resolution: {integrity: sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==} + + '@scure/bip32@2.0.1': + resolution: {integrity: sha512-4Md1NI5BzoVP+bhyJaY3K6yMesEFzNS1sE/cP+9nuvE7p/b0kx9XbpDHHFl8dHtufcbdHRUUQdRqLIPHN/s7yA==} + + '@scure/bip39@2.0.1': + resolution: {integrity: sha512-PsxdFj/d2AcJcZDX1FXN3dDgitDDTmwf78rKZq1a6c1P1Nan1X/Sxc7667zU3U+AN60g7SxxP0YCVw2H/hBycg==} + '@swc/helpers@0.5.23': resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} @@ -1126,6 +1150,17 @@ packages: natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + nostr-tools@2.23.5: + resolution: {integrity: sha512-Fa7ZlUdjfUW1P4E7H3yBexhOHYi18XNyvd2n7eNHkYR085xADX6Y8V8Vm7nT/XQajaFOBrptXmVIGkJ2E4vfVw==} + peerDependencies: + typescript: '>=5.0.0' + peerDependenciesMeta: + typescript: + optional: true + + nostr-wasm@0.1.0: + resolution: {integrity: sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==} + nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -1667,6 +1702,14 @@ snapshots: '@tybys/wasm-util': 0.10.2 optional: true + '@noble/ciphers@2.1.1': {} + + '@noble/curves@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + + '@noble/hashes@2.0.1': {} + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1734,6 +1777,19 @@ snapshots: '@rolldown/pluginutils@1.0.1': {} + '@scure/base@2.0.0': {} + + '@scure/bip32@2.0.1': + dependencies: + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + + '@scure/bip39@2.0.1': + dependencies: + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@swc/helpers@0.5.23': dependencies: tslib: 2.8.1 @@ -2477,6 +2533,20 @@ snapshots: natural-compare@1.4.0: {} + nostr-tools@2.23.5(typescript@6.0.3): + dependencies: + '@noble/ciphers': 2.1.1 + '@noble/curves': 2.0.1 + '@noble/hashes': 2.0.1 + '@scure/base': 2.0.0 + '@scure/bip32': 2.0.1 + '@scure/bip39': 2.0.1 + nostr-wasm: 0.1.0 + optionalDependencies: + typescript: 6.0.3 + + nostr-wasm@0.1.0: {} + nth-check@2.1.1: dependencies: boolbase: 1.0.0 diff --git a/public/images/asheville/01-living.jpg b/public/images/asheville/01-living.jpg index 03d8973..5bbffff 100644 Binary files a/public/images/asheville/01-living.jpg and b/public/images/asheville/01-living.jpg differ diff --git a/public/images/asheville/02-kitchen.jpg b/public/images/asheville/02-kitchen.jpg index ccda9f9..3d1359b 100644 Binary files a/public/images/asheville/02-kitchen.jpg and b/public/images/asheville/02-kitchen.jpg differ diff --git a/public/images/asheville/03-bath.jpg b/public/images/asheville/03-bath.jpg index 6bfdbe3..ae844ab 100644 Binary files a/public/images/asheville/03-bath.jpg and b/public/images/asheville/03-bath.jpg differ diff --git a/public/images/asheville/04-dining-wide.jpg b/public/images/asheville/04-dining-wide.jpg index 3621a40..2edff18 100644 Binary files a/public/images/asheville/04-dining-wide.jpg and b/public/images/asheville/04-dining-wide.jpg differ diff --git a/public/images/asheville/05-dining-detail.jpg b/public/images/asheville/05-dining-detail.jpg index 87b82e9..00674f5 100644 Binary files a/public/images/asheville/05-dining-detail.jpg and b/public/images/asheville/05-dining-detail.jpg differ diff --git a/public/images/boulder/01.jpg b/public/images/boulder/01.jpg new file mode 100644 index 0000000..b9c0ab2 Binary files /dev/null and b/public/images/boulder/01.jpg differ diff --git a/public/images/boulder/02.jpg b/public/images/boulder/02.jpg new file mode 100644 index 0000000..3b36bef Binary files /dev/null and b/public/images/boulder/02.jpg differ diff --git a/public/images/boulder/03.jpg b/public/images/boulder/03.jpg new file mode 100644 index 0000000..8a5bf9c Binary files /dev/null and b/public/images/boulder/03.jpg differ diff --git a/public/images/boulder/04.jpg b/public/images/boulder/04.jpg new file mode 100644 index 0000000..c8881b5 Binary files /dev/null and b/public/images/boulder/04.jpg differ diff --git a/public/images/boulder/05.jpg b/public/images/boulder/05.jpg new file mode 100644 index 0000000..9114b57 Binary files /dev/null and b/public/images/boulder/05.jpg differ diff --git a/public/images/boulder/06.jpg b/public/images/boulder/06.jpg new file mode 100644 index 0000000..e94bce8 Binary files /dev/null and b/public/images/boulder/06.jpg differ diff --git a/public/images/boulder/07.jpg b/public/images/boulder/07.jpg new file mode 100644 index 0000000..ff12658 Binary files /dev/null and b/public/images/boulder/07.jpg differ diff --git a/public/images/boulder/08.jpg b/public/images/boulder/08.jpg new file mode 100644 index 0000000..d0bee43 Binary files /dev/null and b/public/images/boulder/08.jpg differ diff --git a/public/images/boulder/09.jpg b/public/images/boulder/09.jpg new file mode 100644 index 0000000..a651b3e Binary files /dev/null and b/public/images/boulder/09.jpg differ diff --git a/public/images/boulder/10.jpg b/public/images/boulder/10.jpg new file mode 100644 index 0000000..ee5fc2f Binary files /dev/null and b/public/images/boulder/10.jpg differ diff --git a/public/images/boulder/11.jpg b/public/images/boulder/11.jpg new file mode 100644 index 0000000..4cf1508 Binary files /dev/null and b/public/images/boulder/11.jpg differ diff --git a/public/images/boulder/12.jpg b/public/images/boulder/12.jpg new file mode 100644 index 0000000..e554d02 Binary files /dev/null and b/public/images/boulder/12.jpg differ diff --git a/src/components/contact/ContactForm.vue b/src/components/contact/ContactForm.vue new file mode 100644 index 0000000..91478b5 --- /dev/null +++ b/src/components/contact/ContactForm.vue @@ -0,0 +1,220 @@ + + +