Loading...
Loading...
A Palantir-ish dark command-center dashboard for family trip planning with convoy routes, timeline playback, meal logistics, and Google Maps integration.
npx skill4agent add aradotso/trending-skills palantir-for-family-tripsSkill by ara.so — Daily 2026 Skills collection.
git clone https://github.com/andrewjiang/palantir-for-family-trips.git
cd palantir-for-family-trips
npm install
cp .env.example .env.envVITE_GOOGLE_MAPS_API_KEY=your_browser_maps_key_here
VITE_GOOGLE_MAP_ID=your_optional_google_map_idnpm run devhttp://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.
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/tripModel.jstripModel.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 }));
}src/tripModel.jsfamilies: [
// …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
},
],trip.familiessrc/CommandMap.jsxCommandMap// 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)}
/>CommandMap.jsx// 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);
}
}
);App.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} />
));App.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} />
)}// 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>
);
}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>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)}`);
});
});| Variable | Required | Purpose |
|---|---|---|
| Recommended | Enables Google Maps rendering and Directions API |
| Optional | Enables Cloud-based map styling / Advanced Markers |
const apiKey = import.meta.env.VITE_GOOGLE_MAPS_API_KEY;
const mapId = import.meta.env.VITE_GOOGLE_MAP_ID;tripModel.jslocalStorage| 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 |