palantir-for-family-trips

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Family Trip Command Center (Palantir for Family Trips)

家庭旅行指挥中心(家庭版Palantir)

Skill by 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.

ara.so开发的技能项目——属于2026每日技能合集。
这是一款刻意过度构建的深色主题操作仪表盘,用于协调多家庭公路旅行。基于React 19、Vite、Google Maps JS API、Framer Motion和Lucide图标构建。将周末 cabin 旅行当作实时操作室来处理:车队追踪、到达窗口、逐日时间线回放、餐饮后勤、费用分摊以及家庭清单。

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.

bash
git clone https://github.com/andrewjiang/palantir-for-family-trips.git
cd palantir-for-family-trips
npm install
cp .env.example .env
编辑
.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
打开
http://127.0.0.1:5173
(或Vite输出的URL)。
若无Maps API密钥,UI可完全渲染,但实时Google Map图层无法初始化。其他所有视图均可正常使用。

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

src/
  App.jsx          # 主框架、标签页、时间线控件、覆盖层
  CommandMap.jsx   # Google Maps路线渲染、车队回放
  tripModel.js     # 种子旅行文档、家庭信息、路线、辅助函数
  main.jsx         # React入口文件
  index.css        # 全局深色主题样式
public/
docs/              # README中的截图资源
.env.example

Core Data Model (
src/tripModel.js
)

核心数据模型(
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.
旅行信息是一个从
tripModel.js
导出的JS对象(即“旅行文档”)。所有内容——家庭、路线、日期、餐饮、活动、费用——都存储在此处。

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 }));
}

js
// src/tripModel.js (简化版)

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" },
      ],
    },
    // …更多日期
  ],
  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"],
  },
};

// 辅助函数:通过ID获取家庭信息
export function getFamily(id) {
  return trip.families.find((f) => f.id === id);
}

// 辅助函数:获取指定日期字符串"YYYY-MM-DD"的事件
export function getDayEvents(dateStr) {
  const day = trip.days.find((d) => d.date === dateStr);
  return day ? day.events : [];
}

// 辅助函数:计算每个家庭需承担的费用总额
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.

编辑
src/tripModel.js
js
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", // 为地图折线和UI装饰选择独特颜色
  },
],
地图、时间线和家庭视图都会响应式读取
trip.families
——在此处添加家庭后,所有模块都会自动显示该家庭信息。

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

Google Maps与路线回放(
src/CommandMap.jsx

CommandMap
accepts props derived from the trip model and renders routes + convoy playback.
CommandMap
接收来自旅行模型的属性并渲染路线+车队回放。

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)}
/>
jsx
// 在App.jsx中
import CommandMap from "./CommandMap";
import { trip } from "./tripModel";

<CommandMap
  families={trip.families}
  basecamp={trip.basecamp}
  playbackTime={currentPlaybackTime} // ISO字符串或null
  activeFamily={selectedFamilyId}    // 高亮某条车队路线
  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);
    }
  }
);

CommandMap.jsx
中,每个家庭的路线由Directions Service构建。要添加途经点(如加油站):
js
// 在CommandMap.jsx中调用directionsService.route()的位置:
directionsService.route(
  {
    origin: family.origin.coords,
    destination: trip.basecamp.coords,
    waypoints: [
      {
        location: { lat: 37.65, lng: -120.95 }, // 例如Oakdale加油站
        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.
App.jsx
中的时间线会逐步展示每日事件。当前日期索引和回放偏移量决定了地图和时间线面板的显示内容。

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} />
));

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

// 前进到下一天
function advanceDay() {
  setDayIndex((i) => Math.min(i + 1, trip.days.length - 1));
}

// 渲染当前日期的事件
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>
  );
}

标签页在
App.jsx
中定义。要添加新视图(如“打包清单”标签页):
jsx
// 1. 在tabs数组中定义标签页
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" }, // ← 新增
];

// 2. 在标签内容切换逻辑中渲染
{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

Framer Motion覆盖层模式

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 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
      transition={{ duration: 0.3, ease: "easeOut" }}
    >
      <h1>MISSION LAUNCH</h1>
      <button onClick={() => setShowOverlay(false)}>Dismiss</button>
    </motion.div>
  )}
</AnimatePresence>

任务启动覆盖层使用Framer Motion。可复用此模式创建任何全屏动态覆盖层:
jsx
import { AnimatePresence, motion } from "framer-motion";

<AnimatePresence>
  {showOverlay && (
    <motion.div
      className="overlay"
      initial={{ opacity: 0, scale: 0.95 }}
      animate={{ opacity: 1, scale: 1 }}
      exit={{ opacity: 0, scale: 0.95 }}
      transition={{ duration: 0.3, ease: "easeOut" }}
    >
      <h1>MISSION LAUNCH</h1>
      <button onClick={() => setShowOverlay(false)}>Dismiss</button>
    </motion.div>
  )}
</AnimatePresence>

Expense Split Example

费用分摊示例

js
import { trip, splitExpense } from "./tripModel";

// Show each family's share for all expenses
trip.expenses.forEach((exp) => {
  const splits = splitExpense(exp);
  console.log(`${exp.label} ($${exp.amount}):`);
  splits.forEach(({ famId, owes }) => {
    const fam = trip.families.find((f) => f.id === famId);
    console.log(`  ${fam.name} owes $${owes.toFixed(2)}`);
  });
});

js
import { trip, splitExpense } from "./tripModel";

// 显示所有费用中每个家庭需承担的份额
trip.expenses.forEach((exp) => {
  const splits = splitExpense(exp);
  console.log(`${exp.label} ($${exp.amount}):`);
  splits.forEach(({ famId, owes }) => {
    const fam = trip.families.find((f) => f.id === famId);
    console.log(`  ${fam.name} owes $${owes.toFixed(2)}`);
  });
});

Environment Variables Reference

环境变量参考

VariableRequiredPurpose
VITE_GOOGLE_MAPS_API_KEY
RecommendedEnables Google Maps rendering and Directions API
VITE_GOOGLE_MAP_ID
OptionalEnables Cloud-based map styling / Advanced Markers
Access in code:
js
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
const mapId = import.meta.env.VITE_GOOGLE_MAP_ID;

变量是否必填用途
VITE_GOOGLE_MAPS_API_KEY
推荐启用Google Maps渲染和Directions API
VITE_GOOGLE_MAP_ID
可选启用基于云的地图样式/高级标记
在代码中访问:
js
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
const mapId = import.meta.env.VITE_GOOGLE_MAP_ID;

Common Patterns & Tips

常见模式与提示

  • All trip data lives in
    tripModel.js
    — edit there first; the UI reacts automatically.
  • Family colors are used for map polylines, timeline accents, and checklist headers. Pick hex values with enough contrast on dark backgrounds.
  • State is local — no backend, no auth. Refresh resets to seed data unless you add
    localStorage
    persistence.
  • Optimized for desktop — the dense dashboard layout is intentional; don't expect a responsive mobile layout out of the box.
  • Google Maps billing — if you deploy with your key, usage is billed to your Google Cloud project. Restrict the key to your domain.

  • 所有旅行数据存储在
    tripModel.js
    — 先在此处编辑;UI会自动响应变化。
  • 家庭颜色用于地图折线、时间线装饰和清单标题。选择在深色背景上有足够对比度的十六进制值。
  • 状态为本地存储 — 无后端,无认证。刷新页面会重置为种子数据,除非添加
    localStorage
    持久化功能。
  • 针对桌面优化 — 密集的仪表盘布局是有意设计的;默认不支持响应式移动端布局。
  • Google Maps计费 — 若使用你的密钥部署,使用量会计入你的Google Cloud项目账单。请将密钥限制在你的域名下使用。

Troubleshooting

故障排除

ProblemFix
Map doesn't loadCheck
VITE_GOOGLE_MAPS_API_KEY
in
.env
; ensure the key has Maps JS API and Directions API enabled in Google Cloud Console
google is not defined
The Maps script loads async; make sure
CommandMap
only calls Maps APIs inside the
onLoad
callback
Routes don't renderDirections API must be enabled separately from Maps JS API in your Cloud project
Vite dev server URL differsUse whatever URL Vite prints, not a hardcoded port
Adding a family breaks layoutGive the new family a unique
color
and
id
; check that any hardcoded
familyId
references in
App.jsx
are generalized to
trip.families.map(...)
问题解决方法
地图无法加载检查
.env
中的
VITE_GOOGLE_MAPS_API_KEY
;确保该密钥在Google Cloud Console中启用了Maps JS API和Directions API
google is not defined
Maps脚本异步加载;确保
CommandMap
仅在
onLoad
回调中调用Maps API
路线无法渲染在你的Cloud项目中,需单独启用Directions API(与Maps JS API分开)
Vite开发服务器URL不同使用Vite输出的URL,而非硬编码端口
添加家庭后布局异常为新家庭设置唯一的
color
id
;检查
App.jsx
中所有硬编码的
familyId
引用是否已改为
trip.families.map(...)
通用写法