Phase 3: Enhanced Mission page with real progress tracking
- Added lib/data/mission.ts with progress calculations - Transformed Mission page with progress dashboard - 4 progress bars: Freedom, iOS Portfolio, Side Hustle, Travel Fund - Milestones timeline with category badges - Next Steps section with high priority tasks - Shimmer animations on progress bars - Revalidates every 5 minutes
This commit is contained in:
parent
3194737499
commit
86060a0585
@ -124,3 +124,33 @@
|
||||
@apply bg-background text-foreground;
|
||||
}
|
||||
}
|
||||
|
||||
/* Mission Page Animations */
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
transform: translateX(-100%);
|
||||
}
|
||||
100% {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fade-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-shimmer {
|
||||
animation: shimmer 2s infinite;
|
||||
}
|
||||
|
||||
.animate-fade-in {
|
||||
animation: fade-in 0.5s ease-out forwards;
|
||||
opacity: 0;
|
||||
}
|
||||
@ -1,220 +1,425 @@
|
||||
import { DashboardLayout } from "@/components/layout/sidebar";
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Target, TrendingUp, Heart, Plane, DollarSign, Briefcase } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Target,
|
||||
TrendingUp,
|
||||
Heart,
|
||||
Plane,
|
||||
DollarSign,
|
||||
Briefcase,
|
||||
Smartphone,
|
||||
Rocket,
|
||||
CheckCircle2,
|
||||
ArrowRight,
|
||||
Flame,
|
||||
Clock,
|
||||
ExternalLink,
|
||||
} from "lucide-react";
|
||||
import {
|
||||
fetchMissionProgress,
|
||||
ProgressMetric,
|
||||
Milestone,
|
||||
NextStep
|
||||
} from "@/lib/data/mission";
|
||||
|
||||
const missionStatement = {
|
||||
title: "The Mission",
|
||||
description: "Build a sustainable side hustle through iOS apps to achieve financial independence, travel with Heidi, take care of family, and retire on my own terms — all while staying healthy and having fun.",
|
||||
// Revalidate every 5 minutes
|
||||
export const revalidate = 300;
|
||||
|
||||
// ============================================================================
|
||||
// Progress Bar Component
|
||||
// ============================================================================
|
||||
|
||||
function ProgressBar({ metric }: { metric: ProgressMetric }) {
|
||||
const IconComponent = {
|
||||
Target,
|
||||
Smartphone,
|
||||
DollarSign,
|
||||
Plane,
|
||||
Briefcase,
|
||||
Heart,
|
||||
TrendingUp,
|
||||
}[metric.icon] || Target;
|
||||
|
||||
const formattedCurrent = metric.unit === "MRR" || metric.unit === "saved"
|
||||
? new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 0,
|
||||
}).format(metric.current)
|
||||
: metric.current;
|
||||
|
||||
const formattedTarget = metric.unit === "MRR" || metric.unit === "saved"
|
||||
? new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 0,
|
||||
}).format(metric.target)
|
||||
: metric.target;
|
||||
|
||||
return (
|
||||
<Card className="relative overflow-hidden group hover:scale-[1.02] transition-transform duration-300">
|
||||
{/* Background glow effect */}
|
||||
<div className={`absolute inset-0 bg-gradient-to-br ${metric.color} opacity-5 group-hover:opacity-10 transition-opacity`} />
|
||||
|
||||
<CardContent className="p-6 relative z-10">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className={`w-12 h-12 rounded-xl bg-gradient-to-br ${metric.color} flex items-center justify-center shadow-lg`}>
|
||||
<IconComponent className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-bold text-lg">{metric.label}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{formattedCurrent} / {formattedTarget} {metric.unit}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className={`text-3xl font-bold bg-gradient-to-r ${metric.color} bg-clip-text text-transparent`}>
|
||||
{metric.percentage}%
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress bar */}
|
||||
<div className="relative">
|
||||
<div className="h-4 bg-secondary/50 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full bg-gradient-to-r ${metric.color} rounded-full transition-all duration-1000 ease-out relative`}
|
||||
style={{ width: `${metric.percentage}%` }}
|
||||
>
|
||||
{/* Shine effect */}
|
||||
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-white/30 to-transparent animate-shimmer" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Milestone markers */}
|
||||
<div className="flex justify-between mt-2 text-xs text-muted-foreground">
|
||||
<span>Start</span>
|
||||
<span>25%</span>
|
||||
<span>50%</span>
|
||||
<span>75%</span>
|
||||
<span>Goal</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Milestone Timeline Component
|
||||
// ============================================================================
|
||||
|
||||
function MilestoneTimeline({ milestones }: { milestones: Milestone[] }) {
|
||||
if (milestones.length === 0) {
|
||||
return (
|
||||
<Card className="bg-muted/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<Rocket className="w-12 h-12 mx-auto text-muted-foreground mb-4" />
|
||||
<h3 className="font-semibold text-lg mb-2">No Milestones Yet</h3>
|
||||
<p className="text-muted-foreground">
|
||||
Complete tasks tagged with "milestone" or "mission" to see your achievements here!
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const categoryColors: Record<Milestone["category"], string> = {
|
||||
ios: "bg-blue-500",
|
||||
financial: "bg-amber-500",
|
||||
travel: "bg-purple-500",
|
||||
family: "bg-pink-500",
|
||||
business: "bg-emerald-500",
|
||||
};
|
||||
|
||||
const coreValues = [
|
||||
{ name: "Family", icon: Heart, description: "Priority #1 — take care of loved ones" },
|
||||
{ name: "Health", icon: Target, description: "Stay fit, strong, and mobile" },
|
||||
{ name: "Fun", icon: TrendingUp, description: "Enjoy the journey, not just the destination" },
|
||||
{ name: "Adventure", icon: Plane, description: "Travel and experience new things" },
|
||||
];
|
||||
|
||||
const goals = [
|
||||
{
|
||||
id: 1,
|
||||
title: "Double Retirement Savings",
|
||||
current: 50,
|
||||
target: 100,
|
||||
unit: "%",
|
||||
category: "financial",
|
||||
deadline: "Ongoing",
|
||||
status: "in-progress",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: "Build iOS App Empire",
|
||||
current: 6,
|
||||
target: 20,
|
||||
unit: "apps",
|
||||
category: "business",
|
||||
deadline: "2027",
|
||||
status: "in-progress",
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: "Replace Contract Income",
|
||||
current: 5,
|
||||
target: 100,
|
||||
unit: "%",
|
||||
category: "financial",
|
||||
deadline: "2027",
|
||||
status: "in-progress",
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: "Travel with Heidi",
|
||||
current: 0,
|
||||
target: 10,
|
||||
unit: "countries",
|
||||
category: "adventure",
|
||||
deadline: "2028",
|
||||
status: "not-started",
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: "Family Trip with Mom",
|
||||
current: 0,
|
||||
target: 1,
|
||||
unit: "trip",
|
||||
category: "family",
|
||||
deadline: "2026",
|
||||
status: "planning",
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: "Milestone Birthday Trip",
|
||||
current: 0,
|
||||
target: 1,
|
||||
unit: "trip",
|
||||
category: "family",
|
||||
deadline: "2026",
|
||||
status: "planning",
|
||||
},
|
||||
];
|
||||
|
||||
const kpis = [
|
||||
{ label: "Apps Built", value: "6", change: "+6 since Dec 2024", icon: Briefcase },
|
||||
{ label: "Apps Live", value: "2", change: "2 pending LLC", icon: TrendingUp },
|
||||
{ label: "Contract Months Left", value: "13", change: "Renews Mar 2026", icon: DollarSign },
|
||||
{ label: "Morning Streak", value: "7", change: "days consistent", icon: Target },
|
||||
];
|
||||
|
||||
const categoryColors: Record<string, string> = {
|
||||
financial: "bg-green-500/10 text-green-500",
|
||||
business: "bg-blue-500/10 text-blue-500",
|
||||
adventure: "bg-purple-500/10 text-purple-500",
|
||||
family: "bg-pink-500/10 text-pink-500",
|
||||
const categoryLabels: Record<Milestone["category"], string> = {
|
||||
ios: "iOS",
|
||||
financial: "Financial",
|
||||
travel: "Travel",
|
||||
family: "Family",
|
||||
business: "Business",
|
||||
};
|
||||
|
||||
const statusColors: Record<string, string> = {
|
||||
"in-progress": "bg-yellow-500/10 text-yellow-500",
|
||||
"not-started": "bg-gray-500/10 text-gray-500",
|
||||
planning: "bg-blue-500/10 text-blue-500",
|
||||
completed: "bg-green-500/10 text-green-500",
|
||||
const IconComponents: Record<string, React.ComponentType<{ className?: string }>> = {
|
||||
Smartphone,
|
||||
DollarSign,
|
||||
Plane,
|
||||
Heart,
|
||||
Briefcase,
|
||||
};
|
||||
|
||||
export default function MissionPage() {
|
||||
return (
|
||||
<div className="relative">
|
||||
{/* Timeline line */}
|
||||
<div className="absolute left-6 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary via-primary/50 to-transparent" />
|
||||
|
||||
<div className="space-y-4">
|
||||
{milestones.map((milestone, index) => {
|
||||
const IconComponent = IconComponents[milestone.icon] || CheckCircle2;
|
||||
const completedDate = new Date(milestone.completedAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
year: "numeric",
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={milestone.id}
|
||||
className="relative flex gap-4 group animate-fade-in"
|
||||
style={{ animationDelay: `${index * 100}ms` }}
|
||||
>
|
||||
{/* Timeline dot */}
|
||||
<div className={`w-12 h-12 rounded-full ${categoryColors[milestone.category]} flex items-center justify-center shrink-0 shadow-lg z-10 group-hover:scale-110 transition-transform`}>
|
||||
<IconComponent className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
|
||||
{/* Content */}
|
||||
<Card className="flex-1 group-hover:shadow-md transition-shadow">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<Badge className={`${categoryColors[milestone.category]}/10 text-${categoryColors[milestone.category].replace("bg-", "")}`}>
|
||||
{categoryLabels[milestone.category]}
|
||||
</Badge>
|
||||
<span className="text-xs text-muted-foreground flex items-center gap-1">
|
||||
<CheckCircle2 className="w-3 h-3" />
|
||||
{completedDate}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-semibold">{milestone.title}</h4>
|
||||
{milestone.description && (
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{milestone.description}
|
||||
</p>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Next Steps Component
|
||||
// ============================================================================
|
||||
|
||||
function NextSteps({ steps }: { steps: NextStep[] }) {
|
||||
if (steps.length === 0) {
|
||||
return (
|
||||
<Card className="bg-muted/50">
|
||||
<CardContent className="p-8 text-center">
|
||||
<CheckCircle2 className="w-12 h-12 mx-auto text-green-500 mb-4" />
|
||||
<h3 className="font-semibold text-lg mb-2">All Caught Up!</h3>
|
||||
<p className="text-muted-foreground">
|
||||
No high priority tasks. Time to set new goals!
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
{steps.map((step, index) => (
|
||||
<a
|
||||
key={step.id}
|
||||
href={step.ganttBoardUrl}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block group"
|
||||
>
|
||||
<Card className="group-hover:border-primary/50 group-hover:shadow-md transition-all">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
{/* Priority indicator */}
|
||||
<div className={`w-1 h-full min-h-[40px] rounded-full ${
|
||||
step.priority === "urgent" ? "bg-red-500" : "bg-orange-500"
|
||||
}`} />
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex flex-wrap items-center gap-2 mb-1">
|
||||
<Badge
|
||||
variant={step.priority === "urgent" ? "destructive" : "default"}
|
||||
className="text-xs"
|
||||
>
|
||||
{step.priority === "urgent" ? (
|
||||
<><Flame className="w-3 h-3 mr-1" /> Urgent</>
|
||||
) : (
|
||||
"High Priority"
|
||||
)}
|
||||
</Badge>
|
||||
{step.projectName && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
{step.projectName}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h4 className="font-medium truncate group-hover:text-primary transition-colors">
|
||||
{step.title}
|
||||
</h4>
|
||||
{step.dueDate && (
|
||||
<p className="text-xs text-muted-foreground mt-1 flex items-center gap-1">
|
||||
<Clock className="w-3 h-3" />
|
||||
Due {new Date(step.dueDate).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ExternalLink className="w-4 h-4 text-muted-foreground opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Main Page Component
|
||||
// ============================================================================
|
||||
|
||||
export default async function MissionPage() {
|
||||
const progress = await fetchMissionProgress();
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-8">
|
||||
{/* Header */}
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">The Mission</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Your goals, values, and the path to freedom.
|
||||
<div className="text-center sm:text-left">
|
||||
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight">
|
||||
<span className="bg-gradient-to-r from-blue-500 via-purple-500 to-pink-500 bg-clip-text text-transparent">
|
||||
The Mission
|
||||
</span>
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-2 text-lg">
|
||||
Build an iOS empire → retire on our terms → travel with Heidi → 53 is just the start
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Mission Statement */}
|
||||
<Card className="bg-gradient-to-br from-blue-500/5 via-purple-500/5 to-pink-500/5 border-blue-500/20">
|
||||
<CardContent className="p-6">
|
||||
<h2 className="text-xl font-bold mb-3">{missionStatement.title}</h2>
|
||||
<p className="text-lg leading-relaxed text-muted-foreground">
|
||||
{missionStatement.description}
|
||||
{/* Mission Statement Card */}
|
||||
<Card className="relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-blue-500/10 via-purple-500/10 to-pink-500/10" />
|
||||
<div className="absolute inset-0 bg-[radial-gradient(ellipse_at_top_right,_var(--tw-gradient-stops))] from-blue-500/20 via-transparent to-transparent" />
|
||||
<CardContent className="p-6 sm:p-8 relative z-10">
|
||||
<div className="flex flex-col sm:flex-row items-center gap-6">
|
||||
<div className="w-20 h-20 rounded-2xl bg-gradient-to-br from-blue-500 to-purple-600 flex items-center justify-center shadow-xl shrink-0">
|
||||
<Rocket className="w-10 h-10 text-white" />
|
||||
</div>
|
||||
<div className="text-center sm:text-left">
|
||||
<h2 className="text-xl sm:text-2xl font-bold mb-2">Why We Do What We Do</h2>
|
||||
<p className="text-muted-foreground text-lg leading-relaxed max-w-2xl">
|
||||
Build a sustainable side hustle through iOS apps to achieve financial independence,
|
||||
travel with Heidi, take care of family, and retire on my own terms — all while
|
||||
staying healthy and having fun.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
{/* KPIs */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{kpis.map((kpi) => {
|
||||
const Icon = kpi.icon;
|
||||
return (
|
||||
<Card key={kpi.label}>
|
||||
<CardHeader className="flex flex-row items-center justify-between pb-2">
|
||||
<CardTitle className="text-sm font-medium text-muted-foreground">
|
||||
{kpi.label}
|
||||
</CardTitle>
|
||||
<Icon className="w-4 h-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">{kpi.value}</div>
|
||||
<p className="text-xs text-muted-foreground mt-1">{kpi.change}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
{/* Progress Dashboard */}
|
||||
<section>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center">
|
||||
<TrendingUp className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-2xl font-bold">Progress Dashboard</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Tracking the journey to freedom
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
<ProgressBar metric={progress.retirement} />
|
||||
<ProgressBar metric={progress.iosPortfolio} />
|
||||
<ProgressBar metric={progress.sideHustleRevenue} />
|
||||
<ProgressBar metric={progress.travelFund} />
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Two Column Layout: Milestones & Next Steps */}
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-8">
|
||||
{/* Milestones Timeline */}
|
||||
<section>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-amber-500 to-orange-600 flex items-center justify-center">
|
||||
<CheckCircle2 className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold">Milestones Achieved</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{progress.milestones.length} completed
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<MilestoneTimeline milestones={progress.milestones} />
|
||||
</section>
|
||||
|
||||
{/* Next Steps */}
|
||||
<section>
|
||||
<div className="flex items-center gap-3 mb-6">
|
||||
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-red-500 to-pink-600 flex items-center justify-center">
|
||||
<Flame className="w-5 h-5 text-white" />
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="text-2xl font-bold">Next Steps</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
High priority mission tasks
|
||||
</p>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href="https://gantt-board.vercel.app" target="_blank" rel="noopener noreferrer">
|
||||
View All <ArrowRight className="w-4 h-4 ml-1" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
<NextSteps steps={progress.nextSteps} />
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Core Values */}
|
||||
<div>
|
||||
<section>
|
||||
<h2 className="text-xl font-bold mb-4">Core Values</h2>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
{coreValues.map((value) => {
|
||||
const Icon = value.icon;
|
||||
return (
|
||||
<Card key={value.name}>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-4 gap-4">
|
||||
{[
|
||||
{ name: "Family", icon: Heart, color: "from-pink-500 to-rose-500", desc: "Priority #1" },
|
||||
{ name: "Health", icon: Target, color: "from-green-500 to-emerald-500", desc: "Stay strong" },
|
||||
{ name: "Fun", icon: Rocket, color: "from-blue-500 to-cyan-500", desc: "Enjoy the ride" },
|
||||
{ name: "Adventure", icon: Plane, color: "from-purple-500 to-violet-500", desc: "Explore more" },
|
||||
].map((value) => (
|
||||
<Card key={value.name} className="group hover:scale-105 transition-transform">
|
||||
<CardContent className="p-4 text-center">
|
||||
<div className="w-12 h-12 rounded-full bg-primary/10 flex items-center justify-center mx-auto mb-3">
|
||||
<Icon className="w-6 h-6 text-primary" />
|
||||
</div>
|
||||
<h3 className="font-semibold">{value.name}</h3>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{value.description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Goals */}
|
||||
<div>
|
||||
<h2 className="text-xl font-bold mb-4">Goals</h2>
|
||||
<div className="space-y-4">
|
||||
{goals.map((goal) => (
|
||||
<Card key={goal.id}>
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col sm:flex-row sm:items-center gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 flex-wrap">
|
||||
<h3 className="font-semibold">{goal.title}</h3>
|
||||
<Badge className={categoryColors[goal.category]}>
|
||||
{goal.category}
|
||||
</Badge>
|
||||
<Badge className={statusColors[goal.status]}>
|
||||
{goal.status}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
Deadline: {goal.deadline}
|
||||
</p>
|
||||
</div>
|
||||
<div className="sm:w-48">
|
||||
<div className="flex justify-between text-sm mb-1">
|
||||
<span className="text-muted-foreground">Progress</span>
|
||||
<span className="font-medium">
|
||||
{goal.current}/{goal.target} {goal.unit}
|
||||
</span>
|
||||
</div>
|
||||
<div className="h-2 bg-secondary rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-gradient-to-r from-blue-500 to-purple-500 rounded-full transition-all"
|
||||
style={{ width: `${(goal.current / goal.target) * 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`w-12 h-12 rounded-full bg-gradient-to-br ${value.color} flex items-center justify-center mx-auto mb-3 shadow-lg group-hover:shadow-xl transition-shadow`}>
|
||||
<value.icon className="w-6 h-6 text-white" />
|
||||
</div>
|
||||
<h3 className="font-bold">{value.name}</h3>
|
||||
<p className="text-xs text-muted-foreground">{value.desc}</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Quote */}
|
||||
<Card className="bg-gradient-to-r from-slate-800 to-slate-900 border-0">
|
||||
<CardContent className="p-6 text-center">
|
||||
<p className="text-lg italic text-slate-300">
|
||||
"53 is just the start of the best chapter."
|
||||
<Card className="bg-gradient-to-r from-slate-800 to-slate-900 border-0 relative overflow-hidden">
|
||||
<div className="absolute inset-0 bg-[radial-gradient(circle_at_center,_var(--tw-gradient-stops))] from-blue-500/10 via-transparent to-transparent" />
|
||||
<CardContent className="p-8 text-center relative z-10">
|
||||
<p className="text-xl sm:text-2xl italic text-slate-300 font-light">
|
||||
“53 is just the start of the best chapter.”
|
||||
</p>
|
||||
<p className="text-sm text-slate-500 mt-2">— The Mission</p>
|
||||
<p className="text-sm text-slate-500 mt-3">— The Mission</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
409
lib/data/mission.ts
Normal file
409
lib/data/mission.ts
Normal file
@ -0,0 +1,409 @@
|
||||
import { getServiceSupabase } from "@/lib/supabase/client";
|
||||
import { Task, fetchAllTasks } from "./tasks";
|
||||
import { Project, fetchAllProjects } from "./projects";
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface ProgressMetric {
|
||||
id: string;
|
||||
label: string;
|
||||
current: number;
|
||||
target: number;
|
||||
unit: string;
|
||||
percentage: number;
|
||||
icon: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
export interface Milestone {
|
||||
id: string;
|
||||
title: string;
|
||||
description?: string;
|
||||
completedAt: string;
|
||||
category: "ios" | "financial" | "travel" | "family" | "business";
|
||||
icon: string;
|
||||
}
|
||||
|
||||
export interface NextStep {
|
||||
id: string;
|
||||
title: string;
|
||||
priority: "high" | "urgent";
|
||||
projectName?: string;
|
||||
dueDate?: string;
|
||||
ganttBoardUrl: string;
|
||||
}
|
||||
|
||||
export interface MissionProgress {
|
||||
retirement: ProgressMetric;
|
||||
iosPortfolio: ProgressMetric;
|
||||
sideHustleRevenue: ProgressMetric;
|
||||
travelFund: ProgressMetric;
|
||||
milestones: Milestone[];
|
||||
nextSteps: NextStep[];
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Constants
|
||||
// ============================================================================
|
||||
|
||||
// Target goals
|
||||
const TARGET_APPS = 20;
|
||||
const TARGET_REVENUE = 10000; // $10K MRR target
|
||||
const TARGET_TRAVEL_FUND = 50000; // $50K travel fund
|
||||
const TARGET_RETIREMENT_TASKS = 100; // Tasks completed as proxy for progress
|
||||
|
||||
// iOS-related keywords for identifying app projects
|
||||
const IOS_KEYWORDS = ["ios", "app", "swift", "mobile", "iphone", "ipad", "xcode"];
|
||||
|
||||
// ============================================================================
|
||||
// Helper Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Check if a project is iOS-related
|
||||
*/
|
||||
function isIOSProject(project: Project): boolean {
|
||||
const nameLower = project.name.toLowerCase();
|
||||
const descLower = (project.description || "").toLowerCase();
|
||||
return IOS_KEYWORDS.some(keyword =>
|
||||
nameLower.includes(keyword) || descLower.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a task is iOS-related
|
||||
*/
|
||||
function isIOSTask(task: Task): boolean {
|
||||
const titleLower = task.title.toLowerCase();
|
||||
const descLower = (task.description || "").toLowerCase();
|
||||
const hasIOSTag = task.tags.some(tag =>
|
||||
["ios", "app", "swift", "mobile"].includes(tag.toLowerCase())
|
||||
);
|
||||
return hasIOSTag || IOS_KEYWORDS.some(keyword =>
|
||||
titleLower.includes(keyword) || descLower.includes(keyword)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate percentage with bounds
|
||||
*/
|
||||
function calculatePercentage(current: number, target: number): number {
|
||||
if (target === 0) return 0;
|
||||
return Math.min(100, Math.max(0, Math.round((current / target) * 100)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Format currency
|
||||
*/
|
||||
function formatCurrency(amount: number): string {
|
||||
return new Intl.NumberFormat("en-US", {
|
||||
style: "currency",
|
||||
currency: "USD",
|
||||
maximumFractionDigits: 0,
|
||||
}).format(amount);
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Progress Metric Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get retirement progress based on completed tasks as a proxy
|
||||
* In future, this could be based on actual financial data
|
||||
*/
|
||||
export async function getRetirementProgress(): Promise<ProgressMetric> {
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
// Count completed tasks as a proxy for "progress toward freedom"
|
||||
const { count: completedTasks, error } = await supabase
|
||||
.from("tasks")
|
||||
.select("*", { count: "exact", head: true })
|
||||
.eq("status", "done");
|
||||
|
||||
if (error) {
|
||||
console.error("Error calculating retirement progress:", error);
|
||||
}
|
||||
|
||||
const current = completedTasks || 0;
|
||||
const percentage = calculatePercentage(current, TARGET_RETIREMENT_TASKS);
|
||||
|
||||
return {
|
||||
id: "retirement",
|
||||
label: "Freedom Progress",
|
||||
current,
|
||||
target: TARGET_RETIREMENT_TASKS,
|
||||
unit: "tasks",
|
||||
percentage,
|
||||
icon: "Target",
|
||||
color: "from-emerald-500 to-teal-500",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get iOS portfolio progress
|
||||
* Counts completed iOS projects and tasks
|
||||
*/
|
||||
export async function getiOSPortfolioProgress(): Promise<ProgressMetric> {
|
||||
const [projects, tasks] = await Promise.all([
|
||||
fetchAllProjects(),
|
||||
fetchAllTasks(),
|
||||
]);
|
||||
|
||||
// Count iOS projects
|
||||
const iosProjects = projects.filter(isIOSProject);
|
||||
|
||||
// Count completed iOS tasks as additional progress
|
||||
const completedIOSTasks = tasks.filter(t =>
|
||||
isIOSTask(t) && t.status === "done"
|
||||
).length;
|
||||
|
||||
// Weight: 1 project = 1 app, 10 completed tasks = 1 app progress
|
||||
const taskContribution = Math.floor(completedIOSTasks / 10);
|
||||
const current = Math.min(iosProjects.length + taskContribution, TARGET_APPS);
|
||||
const percentage = calculatePercentage(current, TARGET_APPS);
|
||||
|
||||
return {
|
||||
id: "ios-portfolio",
|
||||
label: "iOS Portfolio",
|
||||
current,
|
||||
target: TARGET_APPS,
|
||||
unit: "apps",
|
||||
percentage,
|
||||
icon: "Smartphone",
|
||||
color: "from-blue-500 to-cyan-500",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get side hustle revenue progress
|
||||
* Placeholder - will be replaced with real revenue tracking
|
||||
*/
|
||||
export async function getSideHustleRevenue(): Promise<ProgressMetric> {
|
||||
// Placeholder: For now, calculate based on completed milestones
|
||||
// In future, this will pull from actual revenue data
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
const { data: doneTasks, error } = await supabase
|
||||
.from("tasks")
|
||||
.select("*")
|
||||
.eq("status", "done");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching revenue milestones:", error);
|
||||
}
|
||||
|
||||
// Filter for tasks with revenue or milestone tags
|
||||
const milestones = (doneTasks || []).filter(task => {
|
||||
const tags = (task.tags || []) as string[];
|
||||
return tags.some(tag =>
|
||||
tag.toLowerCase() === "revenue" ||
|
||||
tag.toLowerCase() === "milestone"
|
||||
);
|
||||
});
|
||||
|
||||
// Estimate: each revenue milestone = $500 progress (placeholder logic)
|
||||
const milestoneCount = milestones.length;
|
||||
const estimatedRevenue = milestoneCount * 500;
|
||||
|
||||
return {
|
||||
id: "revenue",
|
||||
label: "Side Hustle Revenue",
|
||||
current: estimatedRevenue,
|
||||
target: TARGET_REVENUE,
|
||||
unit: "MRR",
|
||||
percentage: calculatePercentage(estimatedRevenue, TARGET_REVENUE),
|
||||
icon: "DollarSign",
|
||||
color: "from-amber-500 to-orange-500",
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get travel fund progress
|
||||
* Placeholder - will be replaced with actual savings tracking
|
||||
*/
|
||||
export async function getTravelFundProgress(): Promise<ProgressMetric> {
|
||||
// Placeholder: Calculate based on "travel" tagged completed tasks
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
const { data: doneTasks, error } = await supabase
|
||||
.from("tasks")
|
||||
.select("*")
|
||||
.eq("status", "done");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching travel progress:", error);
|
||||
}
|
||||
|
||||
// Filter for tasks with travel tag
|
||||
const travelTasks = (doneTasks || []).filter(task => {
|
||||
const tags = (task.tags || []) as string[];
|
||||
return tags.some(tag => tag.toLowerCase() === "travel");
|
||||
});
|
||||
|
||||
// Estimate: each travel task = $500 saved (placeholder)
|
||||
const estimatedSavings = travelTasks.length * 500;
|
||||
|
||||
return {
|
||||
id: "travel",
|
||||
label: "Travel Fund",
|
||||
current: estimatedSavings,
|
||||
target: TARGET_TRAVEL_FUND,
|
||||
unit: "saved",
|
||||
percentage: calculatePercentage(estimatedSavings, TARGET_TRAVEL_FUND),
|
||||
icon: "Plane",
|
||||
color: "from-purple-500 to-pink-500",
|
||||
};
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Milestones Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get mission milestones from completed tasks tagged with "milestone" or "mission"
|
||||
*/
|
||||
export async function getMissionMilestones(): Promise<Milestone[]> {
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
// Fetch all completed tasks and filter for tags in code
|
||||
// (Supabase JSON filtering can be tricky with different setups)
|
||||
const { data, error } = await supabase
|
||||
.from("tasks")
|
||||
.select("*")
|
||||
.eq("status", "done");
|
||||
|
||||
if (error) {
|
||||
console.error("Error fetching mission milestones:", error);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Filter for tasks with milestone or mission tags
|
||||
const milestoneTasks = (data || []).filter(task => {
|
||||
const tags = (task.tags || []) as string[];
|
||||
return tags.some(tag =>
|
||||
tag.toLowerCase() === "milestone" ||
|
||||
tag.toLowerCase() === "mission"
|
||||
);
|
||||
});
|
||||
|
||||
// Sort by completion date (updated_at) descending
|
||||
const sortedData = milestoneTasks.sort((a, b) =>
|
||||
new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime()
|
||||
);
|
||||
|
||||
return sortedData.map((task, index) => {
|
||||
// Determine category based on tags and title
|
||||
let category: Milestone["category"] = "business";
|
||||
const titleLower = task.title.toLowerCase();
|
||||
const tagsLower = (task.tags || []).map((t: string) => t.toLowerCase());
|
||||
|
||||
if (tagsLower.includes("ios") || tagsLower.includes("app") || titleLower.includes("app")) {
|
||||
category = "ios";
|
||||
} else if (tagsLower.includes("travel") || titleLower.includes("travel")) {
|
||||
category = "travel";
|
||||
} else if (tagsLower.includes("family") || titleLower.includes("family")) {
|
||||
category = "family";
|
||||
} else if (tagsLower.includes("revenue") || tagsLower.includes("money") || titleLower.includes("$")) {
|
||||
category = "financial";
|
||||
}
|
||||
|
||||
// Determine icon based on category
|
||||
const iconMap: Record<Milestone["category"], string> = {
|
||||
ios: "Smartphone",
|
||||
financial: "DollarSign",
|
||||
travel: "Plane",
|
||||
family: "Heart",
|
||||
business: "Briefcase",
|
||||
};
|
||||
|
||||
return {
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
description: task.description,
|
||||
completedAt: task.updated_at,
|
||||
category,
|
||||
icon: iconMap[category],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Next Steps Functions
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Get next mission steps - high priority tasks that advance the mission
|
||||
*/
|
||||
export async function getNextMissionSteps(): Promise<NextStep[]> {
|
||||
const supabase = getServiceSupabase();
|
||||
|
||||
// Fetch high/urgent priority tasks that are not done
|
||||
const { data: tasks, error: tasksError } = await supabase
|
||||
.from("tasks")
|
||||
.select("*")
|
||||
.in("priority", ["high", "urgent"])
|
||||
.neq("status", "done")
|
||||
.order("due_date", { ascending: true })
|
||||
.limit(6);
|
||||
|
||||
if (tasksError) {
|
||||
console.error("Error fetching next mission steps:", tasksError);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Fetch projects for names
|
||||
const { data: projects, error: projectsError } = await supabase
|
||||
.from("projects")
|
||||
.select("id, name");
|
||||
|
||||
if (projectsError) {
|
||||
console.error("Error fetching projects:", projectsError);
|
||||
}
|
||||
|
||||
const projectMap = new Map((projects || []).map(p => [p.id, p.name]));
|
||||
|
||||
return (tasks || []).map(task => ({
|
||||
id: task.id,
|
||||
title: task.title,
|
||||
priority: task.priority as "high" | "urgent",
|
||||
projectName: projectMap.get(task.project_id),
|
||||
dueDate: task.due_date,
|
||||
ganttBoardUrl: `https://gantt-board.vercel.app/?task=${task.id}`,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Combined Fetch
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Fetch all mission progress data in parallel
|
||||
*/
|
||||
export async function fetchMissionProgress(): Promise<MissionProgress> {
|
||||
const [
|
||||
retirement,
|
||||
iosPortfolio,
|
||||
sideHustleRevenue,
|
||||
travelFund,
|
||||
milestones,
|
||||
nextSteps,
|
||||
] = await Promise.all([
|
||||
getRetirementProgress(),
|
||||
getiOSPortfolioProgress(),
|
||||
getSideHustleRevenue(),
|
||||
getTravelFundProgress(),
|
||||
getMissionMilestones(),
|
||||
getNextMissionSteps(),
|
||||
]);
|
||||
|
||||
return {
|
||||
retirement,
|
||||
iosPortfolio,
|
||||
sideHustleRevenue,
|
||||
travelFund,
|
||||
milestones,
|
||||
nextSteps,
|
||||
};
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user