Crews page
- "Near me" geolocation toggle with haversine distance sort + distance chips
- Crew count + cities social-proof stat header
- Deterministic per-crew vibe-colour accents (no more wall of teal)
- Activity pulse ("🔥 active this week") + OG/Verified/Established rep badges
- Rich empty state ("Claim your local scene") instead of dead "No crews yet"
Crew synergies
- Crew treasury: transparent, non-custodial NIP-75 zap goal (kind 9041) with
live progress bar tallied from zap receipts; funds settle to founder's own
Lightning wallet — no shared wallet or new wallet settings needed
- Crew-hosted events: host a jam/comp as the crew (event a-tag → crew coord),
"Hosted by [crew]" on event pages, Crew Events list on the crew page
- Crew ↔ Lab hiring pipeline: "Open Call / looking-for" flags (filmer, editor,
coach, builder, members) surfaced on the crew page
- Request to join: encrypted NIP-17 DM to founder + admins (no founder-push
bottleneck, no public join event)
- Reverse links: "Crews that skate here" on spot pages
Contacts (0xchat-style + Armada-style privacy)
- New /contacts page with Crew-mates / Follows / Recent DMs / Saved tabs
- Per-contact quick actions: message, zap, view profile, star
- Private encrypted contacts list (NIP-51 kind 30000, NIP-44 encrypted to self)
- Added to burger menu + desktop More menu (gated to logged-in users)
Docs: NIP.md updated with crew treasury, crew-hosted events, looking-for tags,
and the private encrypted contacts list.
Co-authored-by: shakespeare.diy <assistant@shakespeare.diy>
55 lines
1.5 KiB
TypeScript
55 lines
1.5 KiB
TypeScript
import { useCallback, useState } from 'react';
|
|
import type { LatLng } from '@/lib/crewHelpers';
|
|
|
|
interface GeolocationState {
|
|
coords: LatLng | null;
|
|
loading: boolean;
|
|
error: string | null;
|
|
}
|
|
|
|
/**
|
|
* Thin wrapper around the browser Geolocation API for one-shot "where am I"
|
|
* lookups (used by the Crews "Near me" toggle). Never auto-requests — the user
|
|
* must call {@link request} so we don't prompt on page load.
|
|
*/
|
|
export function useGeolocation() {
|
|
const [state, setState] = useState<GeolocationState>({
|
|
coords: null,
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
|
|
const request = useCallback(() => {
|
|
if (!('geolocation' in navigator)) {
|
|
setState((s) => ({ ...s, error: 'Geolocation is not supported on this device.' }));
|
|
return;
|
|
}
|
|
setState((s) => ({ ...s, loading: true, error: null }));
|
|
navigator.geolocation.getCurrentPosition(
|
|
(pos) => {
|
|
setState({
|
|
coords: { lat: pos.coords.latitude, lng: pos.coords.longitude },
|
|
loading: false,
|
|
error: null,
|
|
});
|
|
},
|
|
(err) => {
|
|
setState({
|
|
coords: null,
|
|
loading: false,
|
|
error:
|
|
err.code === err.PERMISSION_DENIED
|
|
? 'Location permission denied.'
|
|
: 'Could not get your location.',
|
|
});
|
|
},
|
|
{ enableHighAccuracy: false, timeout: 10000, maximumAge: 5 * 60 * 1000 },
|
|
);
|
|
}, []);
|
|
|
|
const clear = useCallback(() => {
|
|
setState({ coords: null, loading: false, error: null });
|
|
}, []);
|
|
|
|
return { ...state, request, clear };
|
|
}
|