palantir-for-family-trips
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFamily 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 .envEdit :
.envenv
VITE_GOOGLE_MAPS_API_KEY=your_browser_maps_key_here
VITE_GOOGLE_MAP_ID=your_optional_google_map_idbash
npm run devOpen (or the URL Vite prints).
http://127.0.0.1:5173Without 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编辑 文件:
.envenv
VITE_GOOGLE_MAPS_API_KEY=your_browser_maps_key_here
VITE_GOOGLE_MAP_ID=your_optional_google_map_idbash
npm run dev打开 (或Vite输出的URL)。
http://127.0.0.1:5173若无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.examplesrc/
App.jsx # 主框架、标签页、时间线控件、覆盖层
CommandMap.jsx # Google Maps路线渲染、车队回放
tripModel.js # 种子旅行文档、家庭信息、路线、辅助函数
main.jsx # React入口文件
index.css # 全局深色主题样式
public/
docs/ # README中的截图资源
.env.exampleCore Data Model (src/tripModel.js
)
src/tripModel.js核心数据模型(src/tripModel.js
)
src/tripModel.jsThe trip is a single JS object (the "trip document") exported from . Everything — families, routes, days, meals, activities, expenses — lives here.
tripModel.js旅行信息是一个从导出的JS对象(即“旅行文档”)。所有内容——家庭、路线、日期、餐饮、活动、费用——都存储在此处。
tripModel.jsTypical 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.jsjs
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 reactively — adding a family here surfaces it everywhere.
trip.families编辑:
src/tripModel.jsjs
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.familiesGoogle Maps & Route Playback (src/CommandMap.jsx
)
src/CommandMap.jsxGoogle Maps与路线回放(src/CommandMap.jsx
)
src/CommandMap.jsxCommandMapCommandMapKey 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 , each family's route is built with the Directions Service. To add a waypoint (e.g. a gas stop):
CommandMap.jsxjs
// 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);
}
}
);在中,每个家庭的路线由Directions Service构建。要添加途经点(如加油站):
CommandMap.jsxjs
// 在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 steps through day events. The current day index and playback offset drive what the map and timeline panel show.
App.jsxApp.jsxHooking 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 . To add a new view (e.g. a "Packing" tab):
App.jsxjsx
// 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.jsxjsx
// 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
环境变量参考
| Variable | Required | Purpose |
|---|---|---|
| Recommended | Enables Google Maps rendering and Directions API |
| Optional | Enables 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;| 变量 | 是否必填 | 用途 |
|---|---|---|
| 推荐 | 启用Google Maps渲染和Directions API |
| 可选 | 启用基于云的地图样式/高级标记 |
在代码中访问:
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 — edit there first; the UI reacts automatically.
tripModel.js - 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 persistence.
localStorage - 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.
- 所有旅行数据存储在中 — 先在此处编辑;UI会自动响应变化。
tripModel.js - 家庭颜色用于地图折线、时间线装饰和清单标题。选择在深色背景上有足够对比度的十六进制值。
- 状态为本地存储 — 无后端,无认证。刷新页面会重置为种子数据,除非添加持久化功能。
localStorage - 针对桌面优化 — 密集的仪表盘布局是有意设计的;默认不支持响应式移动端布局。
- Google Maps计费 — 若使用你的密钥部署,使用量会计入你的Google Cloud项目账单。请将密钥限制在你的域名下使用。
Troubleshooting
故障排除
| Problem | Fix |
|---|---|
| Map doesn't load | Check |
| The Maps script loads async; make sure |
| Routes don't render | Directions API must be enabled separately from Maps JS API in your Cloud project |
| Vite dev server URL differs | Use whatever URL Vite prints, not a hardcoded port |
| Adding a family breaks layout | Give the new family a unique |
| 问题 | 解决方法 |
|---|---|
| 地图无法加载 | 检查 |
| Maps脚本异步加载;确保 |
| 路线无法渲染 | 在你的Cloud项目中,需单独启用Directions API(与Maps JS API分开) |
| Vite开发服务器URL不同 | 使用Vite输出的URL,而非硬编码端口 |
| 添加家庭后布局异常 | 为新家庭设置唯一的 |