Claude
Skills
Sign in
Back

palantir-for-family-trips

Included with Lifetime
$97 forever

A Palantir-ish dark command-center dashboard for family trip planning with convoy routes, timeline playback, meal logistics, and Google Maps integration.

Data & Analytics

What this skill does


# Family Trip Command Center (Palantir for Family Trips)

> Skill by [ara.so](https://ara.so) — Daily 2026 Skills collection.

A deliberately overbuilt, dark-themed operations dashboard for coordinating multi-family road trips. Built with React 19, Vite, Google Maps JS API, Framer Motion, and Lucide icons. Treats a weekend cabin trip like a live ops room: convoy tracking, arrival windows, day-by-day timeline playback, meal logistics, expense splits, and family checklists.

---

## Installation & Setup

```bash
git clone https://github.com/andrewjiang/palantir-for-family-trips.git
cd palantir-for-family-trips
npm install
cp .env.example .env
```

Edit `.env`:

```env
VITE_GOOGLE_MAPS_API_KEY=your_browser_maps_key_here
VITE_GOOGLE_MAP_ID=your_optional_google_map_id
```

```bash
npm run dev
```

Open `http://127.0.0.1:5173` (or the URL Vite prints).

> Without a Maps API key the UI fully renders but the live Google Map layer won't initialize. All other views work without it.

---

## Project Structure

```
src/
  App.jsx          # Main shell, tabs, timeline controls, overlays
  CommandMap.jsx   # Google Maps route rendering, convoy playback
  tripModel.js     # Seed trip document, families, routes, helpers
  main.jsx         # React entry point
  index.css        # Global dark-theme styles
public/
docs/              # Screenshot assets for README
.env.example
```

---

## Core Data Model (`src/tripModel.js`)

The trip is a single JS object (the "trip document") exported from `tripModel.js`. Everything — families, routes, days, meals, activities, expenses — lives here.

### Typical shape

```js
// src/tripModel.js (simplified)

export const trip = {
  name: "Pine Mountain Lake & Yosemite 2026",
  basecamp: {
    label: "Pine Mountain Lake Cabin",
    coords: { lat: 37.85, lng: -120.17 },
  },
  families: [
    {
      id: "fam-a",
      name: "The Johnsons",
      origin: { label: "San Francisco, CA", coords: { lat: 37.77, lng: -122.41 } },
      members: ["Alice", "Bob", "Charlie (8)", "Dana (5)"],
      vehicle: "Blue Minivan",
      arrivalWindow: { earliest: "2026-06-27T14:00:00", latest: "2026-06-27T16:00:00" },
      color: "#4ade80",
    },
    {
      id: "fam-b",
      name: "The Garcias",
      origin: { label: "San Jose, CA", coords: { lat: 37.33, lng: -121.88 } },
      members: ["Elena", "Marco", "Sofia (6)"],
      vehicle: "Silver SUV",
      arrivalWindow: { earliest: "2026-06-27T15:30:00", latest: "2026-06-27T17:00:00" },
      color: "#f97316",
    },
  ],
  days: [
    {
      date: "2026-06-27",
      label: "Arrival Day",
      events: [
        { time: "14:00", type: "arrival", familyId: "fam-a", note: "Johnsons ETA" },
        { time: "18:30", type: "meal", mealId: "dinner-fri", note: "Group dinner at basecamp" },
      ],
    },
    // …more days
  ],
  meals: [
    {
      id: "dinner-fri",
      label: "Friday Dinner",
      type: "dinner",
      location: "basecamp",
      assignedTo: "fam-a",
      menu: ["Tacos", "Chips & Guac", "Watermelon"],
      shoppingList: ["tortillas", "ground beef", "salsa"],
    },
  ],
  activities: [
    {
      id: "act-hike-1",
      label: "Mariposa Grove Hike",
      day: "2026-06-28",
      time: "09:00",
      durationHours: 3,
      location: { label: "Mariposa Grove, Yosemite", coords: { lat: 37.5, lng: -119.6 } },
      suitableAges: "5+",
      notes: "Bring water and snacks",
    },
  ],
  expenses: [
    { id: "exp-1", label: "Cabin rental", amount: 900, paidBy: "fam-a", splitAmong: ["fam-a", "fam-b"] },
  ],
  checklist: {
    "fam-a": ["Pack hiking boots", "Download offline maps", "Bring beach towels"],
    "fam-b": ["Bring board games", "Confirm car seats"],
  },
};

// Helper: get a family by id
export function getFamily(id) {
  return trip.families.find((f) => f.id === id);
}

// Helper: get events for a given date string "YYYY-MM-DD"
export function getDayEvents(dateStr) {
  const day = trip.days.find((d) => d.date === dateStr);
  return day ? day.events : [];
}

// Helper: total expense owed per family
export function splitExpense(expense) {
  const share = expense.amount / expense.splitAmong.length;
  return expense.splitAmong.map((famId) => ({ famId, owes: share }));
}
```

---

## Adding a New Family

Edit `src/tripModel.js`:

```js
families: [
  // …existing families
  {
    id: "fam-c",
    name: "The Nguyens",
    origin: { label: "Sacramento, CA", coords: { lat: 38.57, lng: -121.49 } },
    members: ["Linh", "Minh", "Jade (10)", "Owen (7)"],
    vehicle: "Red Crossover",
    arrivalWindow: { earliest: "2026-06-27T13:00:00", latest: "2026-06-27T15:00:00" },
    color: "#a78bfa", // pick a distinct color for map polyline + UI accents
  },
],
```

The map, timeline, and family view all consume `trip.families` reactively — adding a family here surfaces it everywhere.

---

## Google Maps & Route Playback (`src/CommandMap.jsx`)

`CommandMap` accepts props derived from the trip model and renders routes + convoy playback.

### Key props pattern

```jsx
// Inside App.jsx
import CommandMap from "./CommandMap";
import { trip } from "./tripModel";

<CommandMap
  families={trip.families}
  basecamp={trip.basecamp}
  playbackTime={currentPlaybackTime} // ISO string or null
  activeFamily={selectedFamilyId}    // highlight one convoy line
  onMarkerClick={(familyId) => setSelectedFamilyId(familyId)}
/>
```

### Adding a custom waypoint to a route

In `CommandMap.jsx`, each family's route is built with the Directions Service. To add a waypoint (e.g. a gas stop):

```js
// Inside CommandMap.jsx, where directionsService.route() is called:
directionsService.route(
  {
    origin: family.origin.coords,
    destination: trip.basecamp.coords,
    waypoints: [
      {
        location: { lat: 37.65, lng: -120.95 }, // e.g. Oakdale gas stop
        stopover: false,
      },
    ],
    travelMode: google.maps.TravelMode.DRIVING,
  },
  (result, status) => {
    if (status === "OK") {
      directionsRenderer.setDirections(result);
    }
  }
);
```

---

## Timeline Playback

The timeline in `App.jsx` steps through day events. The current day index and playback offset drive what the map and timeline panel show.

### Hooking into timeline state

```jsx
// App.jsx pattern
const [dayIndex, setDayIndex] = useState(0);
const currentDay = trip.days[dayIndex];

// Step forward
function advanceDay() {
  setDayIndex((i) => Math.min(i + 1, trip.days.length - 1));
}

// Render events for current day
currentDay.events.map((event) => (
  <TimelineEvent key={event.time} event={event} families={trip.families} />
));
```

---

## Adding a New Tab / View

Tabs are defined in `App.jsx`. To add a new view (e.g. a "Packing" tab):

```jsx
// 1. Define the tab in the tabs array
const TABS = [
  { id: "itinerary", label: "Itinerary" },
  { id: "map", label: "Map" },
  { id: "meals", label: "Meals" },
  { id: "activities", label: "Activities" },
  { id: "expenses", label: "Expenses" },
  { id: "families", label: "Families" },
  { id: "packing", label: "Packing" }, // ← new
];

// 2. Render it in the tab content switch
{activeTab === "packing" && (
  <PackingView checklist={trip.checklist} families={trip.families} />
)}
```

```jsx
// src/PackingView.jsx
export default function PackingView({ checklist, families }) {
  return (
    <div className="packing-view">
      {families.map((fam) => (
        <div key={fam.id} className="family-checklist">
          <h3 style={{ color: fam.color }}>{fam.name}</h3>
          <ul>
            {(checklist[fam.id] || []).map((item) => (
              <li key={item}>{item}</li>
            ))}
          </ul>
        </div>
      ))}
    </div>
  );
}
```

---

## Framer Motion Overlay Pattern

The mission launch overlay uses Framer Motion. Reuse this pattern for any dramatic full-screen overlay:

```jsx
import { AnimatePresence, motion } from "framer-motion";

<AnimatePresence>
  {showOverlay && (
    <motion.div
      className="overlay"
      initial={{ opacity: 0, scale: 0.95 }

Related in Data & Analytics