townlist source

App.tsx

import { useEffect, useState } from 'react';
import { StatusBar, useWindowDimensions } from 'react-native';
import AsyncStorage from '@react-native-async-storage/async-storage';
import { SafeAreaProvider } from 'react-native-safe-area-context';
import {
  TextArea,
  Input,
  ScrollArea,
  SafeArea,
  Block,
  Button,
  Chip,
  Dialog,
  Image,
  Icon,
  IconButton,
  Tabs,
  Lightbox,
  PlocksProvider,
  Text,
} from '@plocks/ui';
import type { LightboxItem } from '@plocks/ui';

const C = {
  purple: '#5D3291',
  link: '#315AA6',
  ink: '#202020',
  muted: '#6D6D6D',
  line: '#DADADA',
  pale: '#F5F3F8',
  white: '#FFF',
  green: '#24724C',
};
const CATEGORIES = ['All', 'For sale', 'Housing', 'Jobs', 'Services', 'Community'];
type Category = (typeof CATEGORIES)[number];
type Listing = {
  id: string;
  title: string;
  price: number;
  category: Category;
  location: string;
  posted: string;
  description: string;
  emoji: string;
  seller: string;
};
type Inquiry = { id: string; listingId: string; text: string; when: string };
const INITIAL: Listing[] = [
  {
    id: 'bike',
    title: 'Commuter bike, excellent condition',
    price: 220,
    category: 'For sale',
    location: 'Downtown',
    posted: 'today',
    description: 'Reliable city bike with a comfortable seat and new tires. Ready to ride. Pickup near the library.',
    emoji: '🚲',
    seller: 'Alex',
  },
  {
    id: 'chair',
    title: 'Mid century lounge chair',
    price: 145,
    category: 'For sale',
    location: 'Northside',
    posted: 'today',
    description: 'Solid wood lounge chair with fresh upholstery. A few small signs of use, otherwise lovely.',
    emoji: '🪑',
    seller: 'Morgan',
  },
  {
    id: 'apartment',
    title: 'Sunny 1 bedroom near the park',
    price: 1350,
    category: 'Housing',
    location: 'Midtown',
    posted: 'today',
    description: 'Bright one bedroom with balcony, in-unit laundry, and a short walk to the park. Monthly rent shown.',
    emoji: '🏡',
    seller: 'Dana',
  },
  {
    id: 'camera',
    title: 'Film camera and two lenses',
    price: 310,
    category: 'For sale',
    location: 'West End',
    posted: 'yesterday',
    description: 'Well cared for 35mm film camera. Includes a 50mm lens, wide lens, strap, and carrying case.',
    emoji: '📷',
    seller: 'Jamie',
  },
  {
    id: 'desk',
    title: 'Writing desk with drawers',
    price: 85,
    category: 'For sale',
    location: 'East Village',
    posted: 'yesterday',
    description: 'Compact wood desk with two smooth drawers. Perfect for a small workspace.',
    emoji: '🪵',
    seller: 'Riley',
  },
  {
    id: 'job',
    title: 'Part time café team member',
    price: 18,
    category: 'Jobs',
    location: 'Downtown',
    posted: 'yesterday',
    description: 'Neighborhood café looking for a friendly part time teammate. Pay shown per hour. Flexible mornings.',
    emoji: '☕',
    seller: 'Little Corner Café',
  },
  {
    id: 'plant',
    title: 'Large monstera with ceramic pot',
    price: 45,
    category: 'For sale',
    location: 'Northside',
    posted: '2 days ago',
    description: 'Healthy monstera with lots of new growth. Comes with the ceramic pot pictured in the demo.',
    emoji: '🪴',
    seller: 'Taylor',
  },
  {
    id: 'room',
    title: 'Room in friendly shared house',
    price: 790,
    category: 'Housing',
    location: 'Riverside',
    posted: '2 days ago',
    description: 'Private room in a bright shared home. Utilities included. Monthly rent shown.',
    emoji: '🛏️',
    seller: 'Casey',
  },
  {
    id: 'repair',
    title: 'Bike tune ups and repairs',
    price: 35,
    category: 'Services',
    location: 'Midtown',
    posted: '3 days ago',
    description: 'Local bicycle tune ups, tire replacements, and simple repairs. Starting price shown.',
    emoji: '🔧',
    seller: 'Sam',
  },
  {
    id: 'books',
    title: 'Weekend neighborhood book swap',
    price: 0,
    category: 'Community',
    location: 'Riverside',
    posted: '3 days ago',
    description: 'Bring a book, take a book, meet your neighbors. Saturday from 10 AM to noon at the community table.',
    emoji: '📚',
    seller: 'Neighborhood Library',
  },
  {
    id: 'speaker',
    title: 'Portable Bluetooth speaker',
    price: 38,
    category: 'For sale',
    location: 'Downtown',
    posted: '4 days ago',
    description: 'Compact speaker with a long battery life. Includes charging cable.',
    emoji: '🔊',
    seller: 'Chris',
  },
  {
    id: 'tutor',
    title: 'Friendly math tutoring',
    price: 30,
    category: 'Services',
    location: 'Online / local',
    posted: '4 days ago',
    description: 'Patient tutoring for middle and high school math. Hourly rate shown.',
    emoji: '✏️',
    seller: 'Pat',
  },
];
const STORAGE = 'plocks-townlist-example:v1';
const money = (value: number) => (value === 0 ? 'free' : '$' + value.toLocaleString());
const TINTS: Record<string, string> = {
  'For sale': '#ECE8E2',
  Housing: '#E1EAE6',
  Jobs: '#E6E6F0',
  Services: '#E7EBF3',
  Community: '#F2E9E7',
};
const PHOTOS = {
  bicycle: require('./assets/listings/bicycle.webp'),
  chairRoom: require('./assets/listings/chair-room.webp'),
  livingRoom: require('./assets/listings/living-room.webp'),
  apartment: require('./assets/listings/apartment.webp'),
  balcony: require('./assets/listings/balcony.webp'),
  desk: require('./assets/listings/desk.webp'),
  studio: require('./assets/listings/studio.webp'),
  cafe: require('./assets/listings/cafe.webp'),
};
const photo = (id: string, uri: LightboxItem['uri'], title: string): LightboxItem => ({ id, uri, title });
const GALLERIES: Record<string, LightboxItem[]> = {
  bike: [photo('bike', PHOTOS.bicycle, 'Commuter bike')],
  chair: [
    photo('chair-room', PHOTOS.chairRoom, 'Lounge chair in a reading nook'),
    photo('chair-living', PHOTOS.livingRoom, 'Lounge chair in a living room'),
  ],
  apartment: [
    photo('apartment-room', PHOTOS.apartment, 'Bright living room'),
    photo('apartment-balcony', PHOTOS.balcony, 'Balcony with greenery'),
    photo('apartment-living', PHOTOS.livingRoom, 'Living area'),
  ],
  camera: [photo('camera-studio', PHOTOS.studio, 'Camera on a studio desk')],
  desk: [photo('desk', PHOTOS.desk, 'Writing desk'), photo('desk-studio', PHOTOS.studio, 'Desk in a studio')],
  job: [photo('cafe', PHOTOS.cafe, 'Neighborhood café')],
  plant: [photo('plant', PHOTOS.balcony, 'Balcony plants')],
  room: [photo('room', PHOTOS.livingRoom, 'Shared living room'), photo('room-detail', PHOTOS.apartment, 'Interior')],
  repair: [photo('repair-bike', PHOTOS.bicycle, 'Bicycle')],
  books: [photo('books', PHOTOS.chairRoom, 'Reading corner')],
};

function TownlistScreen() {
  const { width } = useWindowDimensions();
  const wide = width >= 880;
  const [tab, setTab] = useState<'browse' | 'saved' | 'post' | 'inbox'>('browse');
  const [category, setCategory] = useState<Category>('All');
  const [query, setQuery] = useState('');
  const [location, setLocation] = useState('');
  const [listings, setListings] = useState<Listing[]>(INITIAL);
  const [saved, setSaved] = useState<string[]>([]);
  const [inquiries, setInquiries] = useState<Inquiry[]>([]);
  const [selected, setSelected] = useState<string | null>(null);
  const [lightboxIndex, setLightboxIndex] = useState<number | null>(null);
  const [message, setMessage] = useState('');
  const [title, setTitle] = useState('');
  const [price, setPrice] = useState('');
  const [postCategory, setPostCategory] = useState<Category>('For sale');
  const [postLocation, setPostLocation] = useState('');
  const [description, setDescription] = useState('');
  const [postEmoji, setPostEmoji] = useState('📦');
  const [ready, setReady] = useState(false);
  const listing = listings.find((item) => item.id === selected);
  const listingPhotos = listing ? GALLERIES[listing.id] ?? [] : [];
  const results = listings.filter(
    (item) =>
      (tab === 'saved' ? saved.includes(item.id) : category === 'All' || item.category === category) &&
      `${item.title} ${item.description}`.toLowerCase().includes(query.toLowerCase()) &&
      item.location.toLowerCase().includes(location.toLowerCase()),
  );

  useEffect(() => {
    AsyncStorage.getItem(STORAGE)
      .then((raw) => {
        if (!raw) return;
        const data = JSON.parse(raw) as { listings?: Listing[]; saved?: string[]; inquiries?: Inquiry[] };
        if (Array.isArray(data.listings)) setListings(data.listings);
        if (Array.isArray(data.saved)) setSaved(data.saved);
        if (Array.isArray(data.inquiries)) setInquiries(data.inquiries);
      })
      .catch(() => {})
      .finally(() => setReady(true));
  }, []);
  useEffect(() => {
    if (ready) AsyncStorage.setItem(STORAGE, JSON.stringify({ listings, saved, inquiries })).catch(() => {});
  }, [listings, saved, inquiries, ready]);
  function createListing() {
    if (!title.trim() || !postLocation.trim() || !description.trim() || !price.trim()) return;
    const amount = Number(price);
    if (!Number.isFinite(amount) || amount < 0) return;
    const next: Listing = {
      id: `local-${Date.now()}`,
      title: title.trim(),
      price: amount,
      category: postCategory,
      location: postLocation.trim(),
      description: description.trim(),
      emoji: postEmoji,
      seller: 'You',
      posted: 'just now',
    };
    setListings((old) => [next, ...old]);
    setTitle('');
    setPrice('');
    setPostLocation('');
    setDescription('');
    setCategory('All');
    setQuery('');
    setLocation('');
    setTab('browse');
    setSelected(next.id);
  }
  function sendInquiry() {
    if (!listing || !message.trim()) return;
    setInquiries((old) => [
      { id: `inquiry-${Date.now()}`, listingId: listing.id, text: message.trim(), when: 'just now' },
      ...old,
    ]);
    setMessage('');
    setSelected(null);
    setTab('inbox');
  }
  const listingRow = (item: Listing) => (
    <Block
      key={item.id}
      onPress={() => setSelected(item.id)}
      accessibilityRole="button"
      accessibilityLabel={`View ${item.title}`}
      direction="row"
      gap={14}
      py={13}
      borderBottomWidth={1}
      borderBottomColor={C.line}
    >
      {GALLERIES[item.id]?.length ? (
        <Image
          src={GALLERIES[item.id][0].uri}
          w={wide ? 110 : 94}
          h={wide ? 88 : 77}
          radius={7}
          resizeMode="cover"
          alt=""
        />
      ) : (
        <Block
          gap={0}
          w={wide ? 110 : 94}
          h={wide ? 88 : 77}
          radius={7}
          bg={TINTS[item.category]}
          align="center"
          justify="center"
        >
          <Text size={wide ? 43 : 38}>{item.emoji}</Text>
        </Block>
      )}
      <Block flex={1} miw={0} gap={4}>
        <Text c={C.link} fw="semibold" size={15} numberOfLines={2}>
          {item.title}
        </Text>
        <Text c={C.ink} fw="bold" size={16}>
          {money(item.price)}
          {item.category === 'Housing' ? '/mo' : item.category === 'Jobs' || item.category === 'Services' ? '/hr' : ''}
        </Text>
        <Text c={C.muted} size={12}>
          {item.location} · {item.posted}
        </Text>
      </Block>
      <Block
        gap={0}
        onPress={(event) => {
          event.stopPropagation();
          setSaved((old) => (old.includes(item.id) ? old.filter((id) => id !== item.id) : [...old, item.id]));
        }}
        accessibilityRole="button"
        accessibilityLabel={saved.includes(item.id) ? 'Remove saved listing' : 'Save listing'}
        p={7}
        alignSelf="flex-start"
      >
        <Icon
          name="heart"
          variant={saved.includes(item.id) ? 'filled' : 'outlined'}
          color={saved.includes(item.id) ? C.purple : C.muted}
          size={20}
        />
      </Block>
    </Block>
  );
  const nav = (
    <Tabs
      navigationOnly
      orientation={wide ? 'vertical' : 'horizontal'}
      variant="chip"
      color={C.purple}
      value={tab}
      onChange={(item) => {
        setTab(item as typeof tab);
        setSelected(null);
      }}
      items={(['browse', 'saved', 'post', 'inbox'] as const).map((item) => ({
        key: item,
        label: item[0].toUpperCase() + item.slice(1),
        subLabel: item === 'inbox' && inquiries.length ? String(inquiries.length) : undefined,
        icon: (
          <Icon
            name={item === 'browse' ? 'search' : item === 'saved' ? 'heart' : item === 'post' ? 'plus' : 'message'}
            color={tab === item ? C.white : C.muted}
            size={wide ? 20 : 19}
          />
        ),
        content: null,
      }))}
    />
  );

  return (
    <SafeArea gap={0} flex={1} bg={C.white}>
      <StatusBar barStyle="dark-content" />
      <Block gap={0} flex={1} direction={wide ? 'row' : 'column'} w="100%" maw={1250} alignSelf="center">
        {wide && (
          <Block w={220} p={18} borderRightWidth={1} borderRightColor={C.line} gap={20}>
            <Text c={C.purple} fw="bold" size={27}>
              townlist
            </Text>
            {nav}
            <Block gap={0} h={1} bg={C.line} />
            <Text c={C.muted} fw="bold" size={11}>
              CATEGORIES
            </Text>
            {CATEGORIES.map((item) => (
              <Block key={item} gap={0} align="flex-start">
                <Chip
                  onPress={() => {
                    setCategory(item);
                    setTab('browse');
                  }}
                  pressed={category === item}
                  variant={category === item ? 'light' : 'subtle'}
                  color={C.purple}
                  size="sm"
                >
                  {item}
                </Chip>
              </Block>
            ))}
            <Block gap={0} flex={1} />
            <Text c={C.muted} size={11}>
              A local classifieds demo
            </Text>
          </Block>
        )}
        <Block gap={0} flex={1}>
          {!wide && (
            <Block
              gap={0}
              h={58}
              direction="row"
              align="center"
              justify="space-between"
              px={18}
              borderBottomWidth={1}
              borderBottomColor={C.line}
            >
              <Text c={C.purple} fw="bold" size={25}>
                townlist
              </Text>
              <Text c={C.muted} size={12}>
                Your neighborhood
              </Text>
            </Block>
          )}
          <ScrollArea flex={1} keyboardShouldPersistTaps="handled" contentProps={{ align: 'center', p: 20, pb: 40 }}>
            <Block gap={0} w="100%" maw={850}>
              {(tab === 'browse' || tab === 'saved') && (
                <>
                  <Text c={C.ink} fw="bold" size={27}>
                    {tab === 'saved' ? 'Saved listings' : category === 'All' ? 'Find it around the corner' : category}
                  </Text>
                  <Text c={C.muted} size={13} mt={3}>
                    {tab === 'saved' ? 'Listings you want to revisit.' : 'Local finds, people, and possibilities.'}
                  </Text>
                  <Block direction={wide ? 'row' : 'column'} gap={9} mt={19}>
                    <Input
                      variant="outline"
                      value={query}
                      onChangeText={setQuery}
                      placeholder="Search listings"
                      mb={0}
                      flex={1}
                      mih={43}
                      radius={7}
                      inputColor={C.ink}
                    />
                    <Input
                      variant="outline"
                      value={location}
                      onChangeText={setLocation}
                      placeholder="Location"
                      mb={0}
                      w={wide ? 180 : '100%'}
                      mih={43}
                      radius={7}
                      inputColor={C.ink}
                    />
                  </Block>
                  {!wide && tab === 'browse' && (
                    <ScrollArea horizontal showsHorizontalScrollIndicator={false} contentProps={{ gap: 8, py: 17 }}>
                      {CATEGORIES.map((item) => (
                        <Chip
                          key={item}
                          onPress={() => setCategory(item)}
                          pressed={category === item}
                          variant={category === item ? 'filled' : 'surface'}
                          color={C.purple}
                          size="sm"
                        >
                          {item}
                        </Chip>
                      ))}
                    </ScrollArea>
                  )}
                  <Block
                    gap={0}
                    direction="row"
                    justify="space-between"
                    mt={wide ? 21 : 4}
                    pb={7}
                    borderBottomWidth={1}
                    borderBottomColor={C.line}
                  >
                    <Text c={C.muted} size={12}>
                      {results.length} {results.length === 1 ? 'result' : 'results'}
                    </Text>
                    <Text c={C.muted} size={12}>
                      newest first
                    </Text>
                  </Block>
                  {results.length ? (
                    results.map(listingRow)
                  ) : (
                    <Block p={50} align="center" gap={8}>
                      <Icon name="search" color={C.muted} size={29} />
                      <Text c={C.muted} ta="center">
                        No listings match. Try another search or category.
                      </Text>
                    </Block>
                  )}
                </>
              )}
              {tab === 'post' && (
                <Block gap={14}>
                  <Text c={C.ink} fw="bold" size={27}>
                    Create a listing
                  </Text>
                  <Text c={C.muted} size={13}>
                    Share something with your neighborhood.
                  </Text>
                  <Text c={C.ink} fw="semibold" size={13}>
                    Title
                  </Text>
                  <Input
                    variant="outline"
                    value={title}
                    onChangeText={setTitle}
                    placeholder="What are you posting?"
                    mb={0}
                    radius={7}
                    inputColor={C.ink}
                  />
                  <Text c={C.ink} fw="semibold" size={13}>
                    Category
                  </Text>
                  <ScrollArea horizontal showsHorizontalScrollIndicator={false} contentProps={{ gap: 8 }}>
                    {CATEGORIES.filter((item) => item !== 'All').map((item) => (
                      <Block
                        gap={0}
                        key={item}
                        onPress={() => setPostCategory(item)}
                        px={13}
                        py={8}
                        radius={16}
                        bg={postCategory === item ? C.purple : C.pale}
                      >
                        <Text c={postCategory === item ? C.white : C.purple} size={12} fw="semibold">
                          {item}
                        </Text>
                      </Block>
                    ))}
                  </ScrollArea>
                  <Block direction="row" gap={10}>
                    <Block flex={1} gap={7}>
                      <Text c={C.ink} fw="semibold" size={13}>
                        Price
                      </Text>
                      <Input
                        variant="outline"
                        value={price}
                        onChangeText={setPrice}
                        keyboardType="decimal-pad"
                        placeholder="0"
                        mb={0}
                        radius={7}
                        inputColor={C.ink}
                      />
                    </Block>
                    <Block flex={1} gap={7}>
                      <Text c={C.ink} fw="semibold" size={13}>
                        Location
                      </Text>
                      <Input
                        variant="outline"
                        value={postLocation}
                        onChangeText={setPostLocation}
                        placeholder="Neighborhood"
                        mb={0}
                        radius={7}
                        inputColor={C.ink}
                      />
                    </Block>
                  </Block>
                  <Text c={C.ink} fw="semibold" size={13}>
                    Choose an icon
                  </Text>
                  <Block direction="row" gap={11}>
                    {['📦', '🚲', '🪑', '🏡', '📚'].map((item) => (
                      <Block
                        gap={0}
                        key={item}
                        onPress={() => setPostEmoji(item)}
                        w={48}
                        h={48}
                        radius={8}
                        align="center"
                        justify="center"
                        bg={postEmoji === item ? '#E8DDF2' : C.pale}
                        borderWidth={postEmoji === item ? 2 : 0}
                        borderColor={C.purple}
                      >
                        <Text size={27}>{item}</Text>
                      </Block>
                    ))}
                  </Block>
                  <Text c={C.ink} fw="semibold" size={13}>
                    Description
                  </Text>
                  <TextArea
                    variant="outline"
                    value={description}
                    onChangeText={setDescription}
                    placeholder="Describe the details..."
                    mb={0}
                    rows={4}
                    radius={7}
                  />
                  <Button
                    title="Publish listing"
                    color={C.purple}
                    onPress={createListing}
                    disabled={
                      !title.trim() ||
                      !price.trim() ||
                      !postLocation.trim() ||
                      !description.trim() ||
                      !Number.isFinite(Number(price)) ||
                      Number(price) < 0
                    }
                  />
                  <Text c={C.muted} size={11}>
                    Your listing stays on this device.
                  </Text>
                </Block>
              )}
              {tab === 'inbox' && (
                <Block gap={13}>
                  <Text c={C.ink} fw="bold" size={27}>
                    Inbox
                  </Text>
                  {inquiries.length === 0 && <Text c={C.muted}>Messages you send to sellers will appear here.</Text>}
                  {inquiries.map((inquiry) => {
                    const item = listings.find((entry) => entry.id === inquiry.listingId);
                    return (
                      <Block
                        key={inquiry.id}
                        onPress={() => setSelected(inquiry.listingId)}
                        p={15}
                        borderWidth={1}
                        borderColor={C.line}
                        radius={8}
                        gap={6}
                      >
                        <Text c={C.link} fw="semibold" size={14}>
                          {item?.title ?? 'Listing'}
                        </Text>
                        <Text c={C.ink} size={13}>
                          You: {inquiry.text}
                        </Text>
                        <Text c={C.muted} size={11}>
                          {inquiry.when} · Demo message stored locally
                        </Text>
                      </Block>
                    );
                  })}
                </Block>
              )}
            </Block>
          </ScrollArea>
          {!wide && (
            <Block gap={0} borderTopWidth={1} borderTopColor={C.line} bg={C.white}>
              {nav}
            </Block>
          )}
        </Block>
      </Block>
      {!!listing && (
        <Dialog opened accessibilityLabel={listing.title} onClose={() => {
          setSelected(null);
          setLightboxIndex(null);
        }}>
          <Block gap={0}>
            <ScrollArea contentProps={{ p: 18, gap: 12 }}>
              <Block gap={0} direction="row" justify="space-between" align="center">
                <Text c={C.muted} size={12}>
                  {listing.posted} · {listing.location}
                </Text>
                <IconButton
                  icon="x"
                  variant="ghost"
                  iconColor={C.ink}
                  accessibilityLabel="Close listing"
                  onPress={() => setSelected(null)}
                />
              </Block>
              {listingPhotos.length ? (
                <>
                  <Block
                    gap={0}
                    onPress={() => setLightboxIndex(0)}
                    accessibilityRole="button"
                    accessibilityLabel={`View photos of ${listing.title}`}
                  >
                    <Image src={listingPhotos[0].uri} w="100%" h={240} radius={7} resizeMode="cover" alt="" />
                  </Block>
                  {listingPhotos.length > 1 && (
                    <ScrollArea horizontal showsHorizontalScrollIndicator={false} contentProps={{ gap: 8 }}>
                      {listingPhotos.map((item, index) => (
                        <Block
                          key={item.id}
                          gap={0}
                          onPress={() => setLightboxIndex(index)}
                          accessibilityRole="button"
                          accessibilityLabel={`Open photo ${index + 1} of ${listingPhotos.length}`}
                        >
                          <Image src={item.uri} w={84} h={68} radius={5} resizeMode="cover" alt="" />
                        </Block>
                      ))}
                    </ScrollArea>
                  )}
                </>
              ) : (
                <Block gap={0} h={170} bg={TINTS[listing.category]} radius={7} align="center" justify="center">
                  <Text size={86}>{listing.emoji}</Text>
                </Block>
              )}
              <Text c={C.link} fw="bold" size={23}>
                {listing.title}
              </Text>
              <Text c={C.ink} fw="bold" size={22}>
                {money(listing.price)}
                {listing.category === 'Housing'
                  ? '/mo'
                  : listing.category === 'Jobs' || listing.category === 'Services'
                    ? '/hr'
                    : ''}
              </Text>
              <Text c={C.muted} size={12}>
                {listing.category} · posted by {listing.seller}
              </Text>
              <Block gap={0} h={1} bg={C.line} />
              <Text c={C.ink} size={14} lh={22}>
                {listing.description}
              </Text>
              <Block
                onPress={() =>
                  setSaved((old) =>
                    old.includes(listing.id) ? old.filter((id) => id !== listing.id) : [...old, listing.id],
                  )
                }
                direction="row"
                align="center"
                gap={8}
                py={8}
              >
                <Icon
                  name="heart"
                  variant={saved.includes(listing.id) ? 'filled' : 'outlined'}
                  color={C.purple}
                  size={19}
                />
                <Text c={C.purple} fw="semibold">
                  {saved.includes(listing.id) ? 'Saved' : 'Save listing'}
                </Text>
              </Block>
              {listing.seller !== 'You' && (
                <Block gap={9} pt={8}>
                  <Text c={C.ink} fw="bold" size={15}>
                    Message {listing.seller}
                  </Text>
                  <TextArea
                    variant="outline"
                    value={message}
                    onChangeText={setMessage}
                    placeholder="Hi, is this still available?"
                    mb={0}
                    rows={3}
                    radius={7}
                  />
                  <Button title="Send demo message" color={C.purple} disabled={!message.trim()} onPress={sendInquiry} />
                  <Text c={C.muted} size={11}>
                    Messages stay on this device and are not sent to sellers.
                  </Text>
                </Block>
              )}
            </ScrollArea>
          </Block>
          <Lightbox
            opened={lightboxIndex !== null && listingPhotos.length > 0}
            images={listingPhotos}
            initialIndex={lightboxIndex ?? 0}
            onClose={() => setLightboxIndex(null)}
            showDownloadButton={false}
            accessibilityLabel={`${listing.title} photos`}
          />
        </Dialog>
      )}
    </SafeArea>
  );
}
export default function App() {
  return (
    <SafeAreaProvider>
      <PlocksProvider theme={{ colorScheme: 'light' }}>
        <TownlistScreen />
      </PlocksProvider>
    </SafeAreaProvider>
  );
}

index.ts

import { registerRootComponent } from 'expo';
import App from './App';

registerRootComponent(App);