Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 46 additions & 5 deletions src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { Routes, Route, Outlet } from "react-router-dom"
import { Button, Icon, Layout } from "@stellar/design-system"
import { Routes, Route, Outlet, NavLink } from "react-router-dom"
import styles from "./App.module.css"
import ConnectAccount from "./components/ConnectAccount"
import ErrorBoundary from "./components/ErrorBoundary"
import ComingSoon from "./components/ComingSoon"
import Footer from "./components/Footer"
Expand Down Expand Up @@ -46,10 +49,48 @@ function App() {
}

const AppLayout: React.FC = () => (
<div className="min-h-screen flex flex-col pt-24">
<NavBar />
<main className="flex-1 relative z-10">
<Outlet />
<div className={styles.AppLayout}>
<Layout.Header
projectId="Scaffold"
projectTitle="Scaffold"
hasThemeSwitch={true}
contentCenter={
<>
<NavLink to="/">
{({ isActive }) => (
<Button variant="tertiary" size="md" disabled={isActive}>
<Icon.Home01 size="md" />
Home
</Button>
)}
</NavLink>
<NavLink to="/treasury">
{({ isActive }) => (
<Button variant="tertiary" size="md" disabled={isActive}>
<Icon.Coins01 size="md" />
Treasury
</Button>
)}
</NavLink>
<NavLink to="/debug">
{({ isActive }) => (
<Button variant="tertiary" size="md" disabled={isActive}>
<Icon.Code02 size="md" />
Contract Explorer
</Button>
)}
</NavLink>
</>
}
contentRight={<ConnectAccount />}
/>

<main>
<Layout.Content>
<Layout.Inset>
<Outlet />
</Layout.Inset>
</Layout.Content>
</main>
<Footer />
</div>
Expand Down
230 changes: 88 additions & 142 deletions src/components/MilestoneTracker.tsx
Original file line number Diff line number Diff line change
@@ -1,151 +1,97 @@
import React, { useState } from "react"
import { useTranslation } from "react-i18next"
import { useCourse } from "../hooks/useCourse"
import styles from "./MilestoneTracker.module.css"
import React, { useState, useEffect } from "react";
import { Icon, Button, Card, Badge } from "@stellar/design-system";

export interface Milestone {
id: number
label: string
lrnReward: number
interface Milestone {
id: number;
label: string;
lrnReward: number;
status: "completed" | "in-progress" | "locked";
txHash?: string;
}

interface MilestoneTrackerProps {
courseId: string
milestones: Milestone[]
courseId: string;
milestones: Milestone[];
}

function MilestoneStep({
courseId,
milestone,
}: {
courseId: string
milestone: Milestone
}) {
const { getCourseProgress, completeMilestone, isCompletingMilestone } =
useCourse()
const progress = getCourseProgress(courseId)
const isCompleted = progress.completedMilestoneIds.includes(milestone.id)
const hasPrevious =
milestone.id <= 1 ||
progress.completedMilestoneIds.includes(milestone.id - 1)
const status = isCompleted
? "completed"
: hasPrevious
? "in_progress"
: "locked"
const txHash: string | undefined = undefined
const { t, i18n } = useTranslation()
export const MilestoneTracker: React.FC<MilestoneTrackerProps> = ({ milestones }) => {
return (
<div className="space-y-6 max-w-lg mx-auto p-4">
{milestones.map((milestone, index) => (
<div key={milestone.id} className="relative flex items-start gap-4 group">
{/* Progress Line */}
{index !== milestones.length - 1 && (
<div
className={`absolute left-4 top-8 w-0.5 h-full ${
milestone.status === "completed" ? "bg-green-500" : "bg-gray-700"
}`}
/>
)}

const [isCompleting, setIsCompleting] = useState(false)
{/* Status Icon */}
<div className="relative z-10 flex-shrink-0">
<div
className={`w-8 h-8 rounded-full flex items-center justify-center border-2 transition-all ${
milestone.status === "completed"
? "bg-green-500 border-green-500 text-white"
: milestone.status === "in-progress"
? "bg-blue-900/30 border-blue-500 text-blue-400"
: "bg-gray-800 border-gray-600 text-gray-500"
}`}
>
{milestone.status === "completed" ? (
<Icon.Check size="sm" />
) : milestone.status === "in-progress" ? (
<div className="w-2 h-2 rounded-full bg-blue-500 animate-pulse" />
) : (
<Icon.Lock01 size="sm" />
)}
</div>
</div>

const handleComplete = async () => {
if (status !== "in_progress") return
{/* Content Card */}
<Card
className={`flex-grow p-4 transition-all border-l-4 ${
milestone.status === "completed"
? "border-l-green-500 bg-green-950/10"
: milestone.status === "in-progress"
? "border-l-blue-500 bg-blue-950/10"
: "border-l-transparent opacity-60"
}`}
>
<div className="flex items-center justify-between mb-2">
<h3 className={`font-semibold ${milestone.status === "locked" ? "text-gray-500" : "text-white"}`}>
{milestone.label}
</h3>
<Badge variant={milestone.status === "completed" ? "success" : "secondary"}>
+{milestone.lrnReward} LRN
</Badge>
</div>

setIsCompleting(true)
try {
// Optimistically UI changes within useCourse
// wait for completion
await completeMilestone(courseId, milestone.id)
} catch (err) {
console.error("Failed to complete milestone:", err)
} finally {
setIsCompleting(false)
}
}

const getIcon = () => {
switch (status) {
case "completed":
return <span className={styles.animCheck}>✅</span>
case "in_progress":
return <span>⏳</span>
case "locked":
return <span>🔒</span>
default:
return <span>🔒</span>
}
}

return (
<div className={`${styles.step} ${styles[status]}`}>
<div className={styles.iconContainer}>{getIcon()}</div>
<div className={styles.content}>
<div className={styles.header}>
<h3 className={styles.title}>{milestone.label}</h3>
<div className={styles.badge}>
{t("home.milestones.lrnReward", {
amount: new Intl.NumberFormat(i18n.language).format(
milestone.lrnReward,
),
})}
</div>
</div>

{status === "locked" && (
<p style={{ fontSize: "0.9rem", color: "#9ca3af", margin: 0 }}>
{t("home.milestones.locked")}
</p>
)}

{status === "in_progress" && (
<div>
<p style={{ fontSize: "0.9rem", color: "#d1d5db", margin: 0 }}>
{t("home.milestones.inProgress")}
</p>
<button
className={styles.actionBtn}
onClick={handleComplete}
disabled={isCompleting || isCompletingMilestone}
>
{isCompleting || isCompletingMilestone
? t("home.milestones.submittingText")
: t("home.milestones.markComplete")}
</button>
</div>
)}

{status === "completed" && (
<div>
<p
style={{
fontSize: "0.9rem",
color: "#10b981",
margin: 0,
fontWeight: 600,
}}
>
{t("home.milestones.completedText")}
</p>
{txHash && (
<a
href={`https://stellar.expert/explorer/testnet/tx/${txHash}`}
target="_blank"
rel="noopener noreferrer"
className={styles.txLink}
>
{t("home.milestones.tx")}: {txHash} ↗
</a>
)}
</div>
)}
</div>
</div>
)
}

export function MilestoneTracker({
courseId,
milestones,
}: MilestoneTrackerProps) {
return (
<div className={styles.container}>
{milestones.map((milestone) => (
<MilestoneStep
key={milestone.id}
courseId={courseId}
milestone={milestone}
/>
))}
</div>
)
}
{milestone.status === "completed" && milestone.txHash && (
<div className="flex items-center gap-2 text-xs text-green-500/80 mt-2">
<Icon.Activity size="xs" />
<a
href={`https://stellar.expert/explorer/testnet/tx/${milestone.txHash}`}
target="_blank"
rel="noopener noreferrer"
className="hover:underline"
>
View on Explorer: {milestone.txHash.slice(0, 8)}...
</a>
</div>
)}

{milestone.status === "in-progress" && (
<div className="mt-3">
<Button size="sm" variant="secondary" className="w-full">
Complete Task
</Button>
</div>
)}
</Card>
</div>
))}
</div>
);
};
Loading