From 0e405eaa00ee87f64bc246dcc9e793cb522c880a Mon Sep 17 00:00:00 2001 From: SkateSpots Date: Sun, 4 Oct 2026 17:45:42 +0700 Subject: [PATCH] Polish Sasquatch bubble; full mint list; link shops to events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sasquatch (the look regressed): I over-corrected last pass by shrinking the bubble on the map and bolting an always-visible ✕ over the avatar on mobile. Restored the full-size bubble + glow on every route (the map only loses the pulse), moved Hide to a desktop-hover ✕ and an EyeOff "Hide assistant" button in the panel header (Settings → Appearance still re-enables it). Mints: the add-mint dialog only offered a hand-picked "Recommended" shortlist. It now also lists the full searchable MintRadar directory (useMintDirectory) below the recommendations. Events: CreateEventPage could only link a skate spot. Added an optional "Link to a Shop" picker (useSkateShops, search, location autofill) that writes ['a','30017:…'] + ['venue-name', …]. --- AGENTS.md | 19 ++- src/components/SasquatchGuide.tsx | 27 ++-- src/components/wallet/MintPrivacyManager.tsx | 73 +++++++++++ src/i18n/batch3/sasquatchUi.ts | 2 + src/i18n/en.ts | 13 ++ src/i18n/es.ts | 13 ++ src/pages/CreateEventPage.tsx | 129 ++++++++++++++++++- 7 files changed, 261 insertions(+), 15 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index c41bf45..4fefe00 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -38,6 +38,8 @@ session with **no other memory of this project**. So: - **Custom protocol**: `NIP.md` is the source of truth for every custom/adopted kind; the constants live in `src/lib/types.ts`. Highlights: - `30818` skate spot · `30017` shop · `31923`/`31925` events + RSVP · `30823` tours + - `CreateEventPage` can link a **shop** as the venue too (not just a spot): + `['a','30017::']` + `['venue-name', ]` (2026-10-04). - `30402` NIP-99 listing = marketplace item, and the **Assertion Event** for trades - `30403` marketplace offer (+ seller response) — see `NIP.md` - `31871` trade attestation (sale/purchase) · `31872` trade confirmation request @@ -588,11 +590,14 @@ session with **no other memory of this project**. So: **Bubble visibility (2026-10-04):** the floating Sasquatch FAB no longer pulses forever — the `animate-ping` halo shows **only when an unread tip exists**, is `motion-reduce:hidden`, and is suppressed on **full-bleed routes** - (`isFullBleed` passed from `Layout`), where the bubble also runs **compact** - (smaller, no glow) so it stops competing with the map. A ✕ on the FAB hides - it (`useSasquatchHidden`, localStorage `sasquatch-hidden`, same-tab synced via - a `sasquatch:hiddenchange` event), and **Settings → Appearance** has a "Show - Sasquatch assistant" toggle to bring it back. + (`isFullBleed` passed from `Layout`). The bubble keeps its **full size/glow** + on every route (do NOT shrink it into a dull compact circle — that looked + bad); the map only loses the pulse. **Hide** is a small ✕ shown on + **desktop hover**, plus an **EyeOff "Hide assistant"** button in the panel + header, plus `useSasquatchHidden` (`sasquatch-hidden`, same-tab synced via a + `sasquatch:hiddenchange` event) and a **Settings → Appearance** "Show + Sasquatch assistant" toggle. (Earlier iteration put an always-visible ✕ over + the avatar on mobile — removed; it was ugly.) - **ContextVM (CVM) server** (`cvm-server/`, Bun): standalone MCP-over-Nostr endpoint for *external* agents. Runs on this machine as a **system** service (`/etc/systemd/system/skatespots-cvm.service`, `sudo systemctl restart @@ -1555,6 +1560,10 @@ session with **no other memory of this project**. So: at `/api/mints/*` — prod proxies via `public/_redirects` (`/api/mints/* → https://mintradar.org/api/mints/:splat 200`), dev via `server.proxy` in `vite.config.ts`. Data is unofficial/rate-limited; adding a mint is native-only. + **Add-mint dialog (2026-10-04):** `MintPrivacyManager`'s add dialog now also + lists the **full MintRadar directory** (searchable, `useMintDirectory`) below + the hand-picked "Recommended" shortlist — previously it only showed the + recommendations. ⚠️ **`vite.config.ts`'s `server.proxy` also covers `/api/nip05` → `https://skate-spots.org` and `/api/ai` → `https://ai.skate-spots.org` (2026-09-28).** Without them the dev server answers those production-only diff --git a/src/components/SasquatchGuide.tsx b/src/components/SasquatchGuide.tsx index 5185f3b..2f881ac 100644 --- a/src/components/SasquatchGuide.tsx +++ b/src/components/SasquatchGuide.tsx @@ -12,7 +12,7 @@ import { useState, useEffect, useRef, useCallback, useMemo } from 'react'; import { Input } from '@/components/ui/input'; import { ScrollArea } from '@/components/ui/scroll-area'; -import { X, Minimize2, HelpCircle, Send, Loader2, Wifi, WifiOff } from 'lucide-react'; +import { X, Minimize2, HelpCircle, Send, Loader2, Wifi, WifiOff, EyeOff } from 'lucide-react'; import { useLocalStorage } from '@/hooks/useLocalStorage'; import { useSasquatchHidden } from '@/hooks/useSasquatchHidden'; import { useCurrentUser } from '@/hooks/useCurrentUser'; @@ -1607,37 +1607,36 @@ Instructions: >
The Sasquatch {/* Pulse only when there's an unread tip, never on the map, and never - under prefers-reduced-motion (the mount animation is the nuisance). */} + under prefers-reduced-motion (the motion is the nuisance). */} {hasUnread && !compact && ( )} {hasUnread && ( )} + {/* Hide is desktop-only hover here; mobile hides via the open panel + (or Settings → Appearance) so the bubble stays uncluttered. */} @@ -1733,6 +1732,16 @@ Instructions:
+ {/* Hide — removes the floating assistant (bring it back from + Settings → Appearance, or the desktop hover ✕ on the bubble). */} + {/* Minimize — shrinks to draggable bubble */}
+ {/* Full directory (MintRadar) — the recommended list above is just a + shortlist; the whole community directory is searchable here. */} +
+ +
+ + setMintQuery(e.target.value)} + placeholder={t('walletUi.mintPrivacy.searchMints')} + className="pl-8 text-xs" + /> +
+
+ {directoryLoading ? ( +

{t('common.loading')}

+ ) : directoryMints.length === 0 ? ( +

{t('walletUi.mintPrivacy.noDirectoryMints')}

+ ) : ( + directoryMints.map((mint) => { + const isAdded = mints.some((m) => m.url.toLowerCase() === mint.url.toLowerCase()); + return ( +
+
+

{mint.name || getCleanMintLabel({ url: mint.url })}

+

{mint.url.replace(/^https?:\/\//, '')}

+
+ {isAdded ? ( + {t('walletUi.mintPrivacy.added')} + ) : ( + + )} +
+ ); + }) + )} +
+

{t('walletUi.mintPrivacy.directoryHint')}

+
+ {/* Trust Warning */} diff --git a/src/i18n/batch3/sasquatchUi.ts b/src/i18n/batch3/sasquatchUi.ts index ad6b1be..9070510 100644 --- a/src/i18n/batch3/sasquatchUi.ts +++ b/src/i18n/batch3/sasquatchUi.ts @@ -30,6 +30,7 @@ const sasquatchUi = { bubbleTitle: 'Press and drag to move · Tap to open', openGuideAria: 'Open Sasquatch guide', hideAria: 'Hide the Sasquatch assistant', + hide: 'Hide assistant', activeUntil: 'Active until {{date}}', platformOwner: 'Platform owner', upgradeTitle: 'Upgrade to Sasquatch Pro', @@ -162,6 +163,7 @@ const sasquatchUi = { bubbleTitle: 'Pulsa y arrastra para mover · Toca para abrir', openGuideAria: 'Abrir la guía del Sasquatch', hideAria: 'Ocultar el asistente Sasquatch', + hide: 'Ocultar asistente', activeUntil: 'Activo hasta el {{date}}', platformOwner: 'Propietario de la plataforma', upgradeTitle: 'Mejora a Sasquatch Pro', diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 0866953..2ac87c7 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -2964,6 +2964,15 @@ placesUi: { invalidEndDesc: 'End time must be after start time.', linkedTo: 'Linked to: {{title}}', linkToSpot: 'Link to a Skate Spot', + linkToShop: 'Link to a Shop', + selectedShop: 'Linked to {{name}}', + selectShopHint: 'Optional — held at or hosted by a skate shop.', + selectShop: 'Select shop', + changeShop: 'Change shop', + hideShops: 'Hide shops', + searchShops: 'Search shops…', + noShopsMatch: 'No shops match your search.', + noShopsAvailable: 'No shops available yet.', linkToTour: 'Link to a Tour (Optional)', linkTourDesc: "Link this event to a tour to show it's part of a larger series. Great for national tours, international adventures, or World Destruction Tour challenge days.", loadingSpots: 'Loading spots...', @@ -5797,6 +5806,10 @@ walletUi: { adding: 'Adding...', orChoose: 'Or choose from list', recommended: 'Recommended Mints', + allMints: 'All mints (directory)', + searchMints: 'Search mints…', + noDirectoryMints: 'No mints match your search.', + directoryHint: 'From the community MintRadar directory — unofficial and may be stale.', added: 'Added', soon: 'Soon', autoDistribute: 'Auto-distribute funds', diff --git a/src/i18n/es.ts b/src/i18n/es.ts index 980af66..6ab1535 100644 --- a/src/i18n/es.ts +++ b/src/i18n/es.ts @@ -2964,6 +2964,15 @@ placesUi: { invalidEndDesc: 'La hora de fin debe ser posterior a la de inicio.', linkedTo: 'Vinculado a: {{title}}', linkToSpot: 'Vincular a un spot', + linkToShop: 'Vincular a una tienda', + selectedShop: 'Vinculado a {{name}}', + selectShopHint: 'Opcional — se celebra en o lo organiza una tienda de skate.', + selectShop: 'Elegir tienda', + changeShop: 'Cambiar tienda', + hideShops: 'Ocultar tiendas', + searchShops: 'Buscar tiendas…', + noShopsMatch: 'Ninguna tienda coincide con tu búsqueda.', + noShopsAvailable: 'Aún no hay tiendas disponibles.', linkToTour: 'Vincular a un tour (opcional)', linkTourDesc: 'Vincula este evento a un tour para mostrar que forma parte de una serie más amplia. Ideal para tours nacionales, aventuras internacionales o días de retos del World Destruction Tour.', loadingSpots: 'Cargando spots...', @@ -5797,6 +5806,10 @@ walletUi: { adding: 'Añadiendo...', orChoose: 'O elige de la lista', recommended: 'Mints recomendados', + allMints: 'Todos los mints (directorio)', + searchMints: 'Buscar mints…', + noDirectoryMints: 'Ningún mint coincide con tu búsqueda.', + directoryHint: 'Del directorio comunitario MintRadar — no oficial y puede estar desactualizado.', added: 'Añadido', soon: 'Pronto', autoDistribute: 'Distribuir fondos automáticamente', diff --git a/src/pages/CreateEventPage.tsx b/src/pages/CreateEventPage.tsx index 36dc0bb..906f378 100644 --- a/src/pages/CreateEventPage.tsx +++ b/src/pages/CreateEventPage.tsx @@ -15,10 +15,11 @@ import { useCurrentUser } from '@/hooks/useCurrentUser'; import { useUploadFile } from '@/hooks/useUploadFile'; import { useTours } from '@/hooks/useTours'; import { useSkateSpots } from '@/hooks/useSkateSpots'; +import { useSkateShops } from '@/hooks/useSkateShops'; import { useToast } from '@/hooks/useToast'; import { useTranslation } from '@/contexts/language'; import { LoginArea } from '@/components/auth/LoginArea'; -import { CALENDAR_EVENT_KIND, EVENT_TYPE_LABELS, TOUR_CATEGORIES, SKATE_TOUR_KIND, SKATE_SPOT_KIND, SKATE_CREW_KIND, SKATE_MUSICIAN_KIND, SPOT_CATEGORIES, EVENT_SPONSOR_TIERS, eventSponsorTags, type SkateTour, type SkateSpot, type EventSponsor } from '@/lib/types'; +import { CALENDAR_EVENT_KIND, EVENT_TYPE_LABELS, TOUR_CATEGORIES, SKATE_TOUR_KIND, SKATE_SPOT_KIND, SKATE_SHOP_KIND, SKATE_CREW_KIND, SKATE_MUSICIAN_KIND, SPOT_CATEGORIES, EVENT_SPONSOR_TIERS, eventSponsorTags, type SkateTour, type SkateSpot, type EventSponsor } from '@/lib/types'; import { useCrew } from '@/hooks/useCrews'; import { useLabProfile } from '@/hooks/useLabProfiles'; import { Users } from 'lucide-react'; @@ -40,6 +41,7 @@ import { Music, Sparkles, Plus, + Store, } from 'lucide-react'; import { FeaturedEventUpgrade } from '@/components/FeaturedEventUpgrade'; import { usePlatformFee } from '@/hooks/usePlatformFee'; @@ -90,6 +92,8 @@ export default function CreateEventPage() { // Fetch available spots for linking const { data: spots, isLoading: spotsLoading } = useSkateSpots(200); + // Fetch available shops for linking (an event can be held at / hosted by a shop) + const { data: shops, isLoading: shopsLoading } = useSkateShops(200); // Form state const [title, setTitle] = useState(''); @@ -114,6 +118,11 @@ export default function CreateEventPage() { const [showSpotSelector, setShowSpotSelector] = useState(false); const [spotSearchQuery, setSpotSearchQuery] = useState(''); + // Optional shop link — an event can be held at / hosted by a skate shop. + const [linkedShopId, setLinkedShopId] = useState('none'); + const [showShopSelector, setShowShopSelector] = useState(false); + const [shopSearchQuery, setShopSearchQuery] = useState(''); + // Paid event / entry fee const [entryFee, setEntryFee] = useState(''); const isPaidEvent = entryFee.trim().length > 0 && entryFee.trim() !== '0'; @@ -137,6 +146,13 @@ export default function CreateEventPage() { // Get the selected spot (if any) const selectedSpot = spots?.find((s) => `${s.pubkey}:${s.identifier}` === linkedSpotId); + // Get the selected shop (if any) + const selectedShop = shops?.find((s) => `${s.pubkey}:${s.identifier}` === linkedShopId); + const filteredShops = shops?.filter((shop) => { + if (!shopSearchQuery) return true; + const q = shopSearchQuery.toLowerCase(); + return shop.name.toLowerCase().includes(q) || (shop.location?.toLowerCase().includes(q) ?? false); + }); // Filter spots by search query const filteredSpots = spots?.filter((spot) => { @@ -396,6 +412,12 @@ export default function CreateEventPage() { } } + // Optional shop link (venue / host shop) — same addressable ref pattern. + if (selectedShop && linkedShopId !== 'none') { + tags.push(['a', `${SKATE_SHOP_KIND}:${selectedShop.pubkey}:${selectedShop.identifier}`]); + tags.push(['venue-name', selectedShop.name]); + } + // Crew host — link the event back to the crew coordinate so it shows on // the crew page and on the event as "Hosted by [crew]". if (hostCrew) { @@ -829,6 +851,111 @@ export default function CreateEventPage() { )} + {/* Optional shop link — an event can be held at / hosted by a shop */} +
+
+
+ +

+ {selectedShop ? t('placesUi.createEvent.selectedShop', { name: selectedShop.name }) : t('placesUi.createEvent.selectShopHint')} +

+
+ +
+ + {selectedShop && !showShopSelector && ( +
+ {selectedShop.images[0] ? ( + + ) : ( +
+ +
+ )} +
+

{selectedShop.name}

+ {selectedShop.location &&

{selectedShop.location}

} +
+ +
+ )} + + {showShopSelector && ( +
+ setShopSearchQuery(e.target.value)} className="border-[#ff0040]/30 bg-black" /> +
+ {shopsLoading ? ( +
+ +
+ ) : filteredShops && filteredShops.length > 0 ? ( + filteredShops.map((shop) => ( + + )) + ) : ( +

+ {shopSearchQuery ? t('placesUi.createEvent.noShopsMatch') : t('placesUi.createEvent.noShopsAvailable')} +

+ )} +
+
+ )} +
+ {/* Event Types */}