277 lines
7.1 KiB
TypeScript
277 lines
7.1 KiB
TypeScript
/**
|
|
* rBnb Automerge document schemas.
|
|
*
|
|
* Community hospitality — trust-based space sharing and couch surfing.
|
|
* Granularity: one Automerge document per space (listings + stays + endorsements).
|
|
* DocId format: {space}:bnb:listings
|
|
*/
|
|
|
|
import type { DocSchema } from '../../shared/local-first/document';
|
|
|
|
// ── Economy model ──
|
|
|
|
export type EconomyModel = 'gift' | 'suggested' | 'fixed' | 'sliding_scale' | 'exchange';
|
|
|
|
// ── Listing types ──
|
|
|
|
export type ListingType =
|
|
| 'couch'
|
|
| 'room'
|
|
| 'apartment'
|
|
| 'cabin'
|
|
| 'tent_site'
|
|
| 'land'
|
|
| 'studio'
|
|
| 'loft'
|
|
| 'house'
|
|
| 'other';
|
|
|
|
// ── Stay request status ──
|
|
|
|
export type StayStatus =
|
|
| 'pending'
|
|
| 'accepted'
|
|
| 'declined'
|
|
| 'cancelled'
|
|
| 'completed'
|
|
| 'endorsed';
|
|
|
|
// ── Endorsement visibility ──
|
|
|
|
export type EndorsementVisibility = 'public' | 'private' | 'community';
|
|
|
|
// ── Core types ──
|
|
|
|
export interface AvailabilityWindow {
|
|
id: string;
|
|
listingId: string;
|
|
startDate: number; // epoch ms (start of day)
|
|
endDate: number; // epoch ms (end of day)
|
|
status: 'available' | 'blocked' | 'tentative';
|
|
notes: string | null;
|
|
createdAt: number;
|
|
}
|
|
|
|
export interface Listing {
|
|
id: string;
|
|
hostDid: string; // DID of the host
|
|
hostName: string;
|
|
title: string;
|
|
description: string;
|
|
type: ListingType;
|
|
economy: EconomyModel;
|
|
|
|
// Pricing (relevant for non-gift economies)
|
|
suggestedAmount: number | null; // suggested/fixed price per night
|
|
currency: string | null; // e.g. 'USD', 'EUR', or null for gift
|
|
slidingMin: number | null; // sliding scale minimum
|
|
slidingMax: number | null; // sliding scale maximum
|
|
exchangeDescription: string | null; // what the host wants in exchange
|
|
|
|
// Location
|
|
locationName: string;
|
|
locationLat: number | null;
|
|
locationLng: number | null;
|
|
locationGranularity: string | null; // 'city', 'neighborhood', 'address'
|
|
|
|
// Capacity & details
|
|
guestCapacity: number;
|
|
bedroomCount: number | null;
|
|
bedCount: number | null;
|
|
bathroomCount: number | null;
|
|
amenities: string[]; // e.g. ['wifi', 'kitchen', 'laundry', 'parking']
|
|
houseRules: string[]; // e.g. ['no_smoking', 'quiet_hours', 'shoes_off']
|
|
photos: string[]; // asset IDs or URLs
|
|
coverPhoto: string | null;
|
|
|
|
// Trust & auto-accept
|
|
trustThreshold: number | null; // 0-100, rNetwork trust score for auto-accept
|
|
instantAccept: boolean; // if true + trust met → auto-accept requests
|
|
|
|
// Metadata
|
|
isActive: boolean;
|
|
createdAt: number;
|
|
updatedAt: number;
|
|
}
|
|
|
|
export interface StayMessage {
|
|
id: string;
|
|
senderDid: string;
|
|
senderName: string;
|
|
body: string;
|
|
sentAt: number;
|
|
}
|
|
|
|
export interface StayRequest {
|
|
id: string;
|
|
listingId: string;
|
|
guestDid: string;
|
|
guestName: string;
|
|
hostDid: string;
|
|
|
|
// Dates
|
|
checkIn: number; // epoch ms
|
|
checkOut: number; // epoch ms
|
|
guestCount: number;
|
|
|
|
// Status flow: pending → accepted/declined → completed → endorsed
|
|
status: StayStatus;
|
|
|
|
// Messages embedded in the CRDT (conversation lives in the request)
|
|
messages: StayMessage[];
|
|
|
|
// Contribution (for non-gift economies)
|
|
offeredAmount: number | null;
|
|
offeredCurrency: string | null;
|
|
offeredExchange: string | null;
|
|
|
|
// Timestamps
|
|
requestedAt: number;
|
|
respondedAt: number | null;
|
|
completedAt: number | null;
|
|
cancelledAt: number | null;
|
|
}
|
|
|
|
export interface Endorsement {
|
|
id: string;
|
|
stayId: string;
|
|
listingId: string;
|
|
|
|
// Who wrote it and about whom
|
|
authorDid: string;
|
|
authorName: string;
|
|
subjectDid: string; // the person being endorsed (host or guest)
|
|
subjectName: string;
|
|
direction: 'guest_to_host' | 'host_to_guest';
|
|
|
|
// Content
|
|
body: string;
|
|
rating: number | null; // 1-5, optional (endorsements > ratings)
|
|
tags: string[]; // e.g. ['welcoming', 'clean', 'great_conversation']
|
|
visibility: EndorsementVisibility;
|
|
|
|
// Trust integration — feeds into rNetwork trust graph
|
|
trustWeight: number; // 0-1, how much this endorsement affects trust score
|
|
|
|
createdAt: number;
|
|
}
|
|
|
|
export interface SpaceConfig {
|
|
defaultEconomy: EconomyModel;
|
|
defaultTrustThreshold: number; // 0-100
|
|
amenityCatalog: string[]; // available amenities for this community
|
|
houseRuleCatalog: string[]; // available house rules
|
|
endorsementTagCatalog: string[]; // available endorsement tags
|
|
requireEndorsement: boolean; // whether both parties must endorse after stay
|
|
maxStayDays: number; // maximum stay length in days
|
|
}
|
|
|
|
// ── Top-level document ──
|
|
|
|
export interface BnbDoc {
|
|
meta: {
|
|
module: string;
|
|
collection: string;
|
|
version: number;
|
|
spaceSlug: string;
|
|
createdAt: number;
|
|
};
|
|
config: SpaceConfig;
|
|
listings: Record<string, Listing>;
|
|
availability: Record<string, AvailabilityWindow>;
|
|
stays: Record<string, StayRequest>;
|
|
endorsements: Record<string, Endorsement>;
|
|
}
|
|
|
|
// ── Schema registration ──
|
|
|
|
export const DEFAULT_AMENITIES = [
|
|
'wifi', 'kitchen', 'laundry', 'parking', 'garden', 'workspace',
|
|
'heating', 'air_conditioning', 'hot_water', 'towels', 'linens',
|
|
'pets_welcome', 'wheelchair_accessible', 'bike_storage',
|
|
];
|
|
|
|
export const DEFAULT_HOUSE_RULES = [
|
|
'no_smoking', 'quiet_hours', 'shoes_off', 'no_parties',
|
|
'clean_up_after', 'no_pets', 'check_in_by_10pm',
|
|
];
|
|
|
|
export const DEFAULT_ENDORSEMENT_TAGS = [
|
|
'welcoming', 'clean', 'great_conversation', 'respectful',
|
|
'generous', 'good_communication', 'safe_space', 'cozy',
|
|
'well_located', 'quiet', 'fun', 'helpful',
|
|
];
|
|
|
|
const DEFAULT_CONFIG: SpaceConfig = {
|
|
defaultEconomy: 'gift',
|
|
defaultTrustThreshold: 30,
|
|
amenityCatalog: DEFAULT_AMENITIES,
|
|
houseRuleCatalog: DEFAULT_HOUSE_RULES,
|
|
endorsementTagCatalog: DEFAULT_ENDORSEMENT_TAGS,
|
|
requireEndorsement: false,
|
|
maxStayDays: 30,
|
|
};
|
|
|
|
export const bnbSchema: DocSchema<BnbDoc> = {
|
|
module: 'bnb',
|
|
collection: 'listings',
|
|
version: 1,
|
|
init: (): BnbDoc => ({
|
|
meta: {
|
|
module: 'bnb',
|
|
collection: 'listings',
|
|
version: 1,
|
|
spaceSlug: '',
|
|
createdAt: Date.now(),
|
|
},
|
|
config: { ...DEFAULT_CONFIG },
|
|
listings: {},
|
|
availability: {},
|
|
stays: {},
|
|
endorsements: {},
|
|
}),
|
|
};
|
|
|
|
// ── Helpers ──
|
|
|
|
export function bnbDocId(space: string) {
|
|
return `${space}:bnb:listings` as const;
|
|
}
|
|
|
|
/** Economy model display labels */
|
|
export const ECONOMY_LABELS: Record<EconomyModel, string> = {
|
|
gift: 'Gift Economy',
|
|
suggested: 'Suggested Contribution',
|
|
fixed: 'Fixed Price',
|
|
sliding_scale: 'Sliding Scale',
|
|
exchange: 'Skill/Service Exchange',
|
|
};
|
|
|
|
/** Listing type display labels */
|
|
export const LISTING_TYPE_LABELS: Record<ListingType, string> = {
|
|
couch: 'Couch',
|
|
room: 'Private Room',
|
|
apartment: 'Apartment',
|
|
cabin: 'Cabin',
|
|
tent_site: 'Tent Site',
|
|
land: 'Land',
|
|
studio: 'Studio',
|
|
loft: 'Loft',
|
|
house: 'House',
|
|
other: 'Other',
|
|
};
|
|
|
|
/** Listing type icons */
|
|
export const LISTING_TYPE_ICONS: Record<ListingType, string> = {
|
|
couch: '\u{1F6CB}', // couch
|
|
room: '\u{1F6CF}', // bed
|
|
apartment: '\u{1F3E2}', // building
|
|
cabin: '\u{1F3E1}', // house
|
|
tent_site: '\u{26FA}', // tent
|
|
land: '\u{1F333}', // tree
|
|
studio: '\u{1F3A8}', // palette
|
|
loft: '\u{1F3D7}', // building construction
|
|
house: '\u{1F3E0}', // house
|
|
other: '\u{1F3E8}', // hotel
|
|
};
|