Fix/ Step Comparing Step Restriction (#5)

* feat: add learning website with Next.js

Add interactive web interface for the Build Your Own OpenClaw tutorial:
- Next.js app with TypeScript and Tailwind CSS
- Theme provider with dark/light mode toggle
- Step card components with diff visualization
- Utilities for loading step metadata and file diffs

* chore: ignore worktrees directory

* feat(web): implement learning website with step navigation and diff viewer

- Landing page with hero section and steps overview grouped by phase
- Step detail pages with README rendering and syntax highlighting (Shiki)
- Diff comparison pages with side-by-side GitHub-style viewer
- Custom 404 Not Found page
- Components: ReadmeRenderer, CodeBlock, DiffViewer, DiffSelector
- Generates 175 static pages (18 steps + 153 diff combinations)

* feat(web): enhance UI with zane-portfolio theme and improved UX

- Update color scheme to match zane-portfolio style (warm beige light, pure black dark)
- Add scrollbar styling for light/dark modes
- Fix card border visibility in dark mode
- Add GitHub CTA section with clone command and copy button
- Add star on GitHub call-to-action
- Use favicons from zane-portfolio
- Improve diff viewer with scroll sync and collapsible files
- Add sticky header with file navigation dropdown
- Rewrite README links to GitHub and internal step pages
- Remove default create-next-app template files

* ci: add GitHub Pages deployment workflow

- Add workflow to build and deploy Next.js static site
- Configure custom domain (build-your-own-openclaw.kiyo-n-zane.com)
- Trigger on push to main branch

* fix config

* fix config

* fix config

* fix web navigation bug
This commit is contained in:
Zane Chen
2026-03-14 22:06:52 -04:00
committed by GitHub
parent 131150d84d
commit 1b013e7e21
5 changed files with 104 additions and 48 deletions
+13 -31
View File
@@ -1,9 +1,10 @@
import { notFound } from 'next/navigation'
import Link from 'next/link'
import { getStep, getSteps } from '@/lib/steps'
import { getChangedFiles, getUnchangedFiles, type FileDiff } from '@/lib/files'
import { getChangedFiles, getUnchangedFiles } from '@/lib/files'
import { DiffViewer } from '@/components/diff-viewer'
import { FileNavDropdown } from '@/components/file-nav-dropdown'
import { DiffPageSelector } from '@/components/diff-page-selector'
import {
Breadcrumb,
BreadcrumbList,
@@ -12,8 +13,6 @@ import {
BreadcrumbPage,
BreadcrumbSeparator,
} from '@/components/ui/breadcrumb'
import { H2, Muted } from '@/components/ui/typography'
import { PlusIcon, MinusIcon } from 'lucide-react'
interface DiffPageProps {
params: Promise<{
@@ -29,10 +28,10 @@ export async function generateStaticParams() {
// Generate all valid combinations where from < to
for (let i = 0; i < steps.length - 1; i++) {
for (let j = i + 1; j < steps.length; j++) {
params.push({
id: steps[i].id,
to: steps[j].id,
})
params.push({
id: steps[i].id,
to: steps[j].id,
})
}
}
@@ -53,21 +52,6 @@ export async function generateMetadata({ params }: DiffPageProps) {
}
}
// Status type for changed files (excludes 'unchanged')
type ChangedStatus = 'added' | 'removed' | 'modified'
// Get status icon for file
function getStatusIcon(status: ChangedStatus) {
switch (status) {
case 'added':
return <PlusIcon className="size-3 text-green-500" />
case 'removed':
return <MinusIcon className="size-3 text-red-500" />
case 'modified':
return null
}
}
export default async function DiffPage({ params }: DiffPageProps) {
const { id, to } = await params
const fromStep = getStep(id)
@@ -112,15 +96,13 @@ export default async function DiffPage({ params }: DiffPageProps) {
{/* Sticky Page Header */}
<div className="sticky top-14 z-40 -mx-4 px-4 py-4 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60 border-b mb-8">
<div className="flex items-center justify-between">
<div>
<H2 className="mb-1">
Step {fromStep.id} to Step {toStep.id}
</H2>
<Muted>
Comparing &quot;{fromStep.title}&quot; with &quot;{toStep.title}&quot;
</Muted>
</div>
<div className="flex items-center justify-between gap-4">
<DiffPageSelector
fromStep={fromStep}
toStep={toStep}
fromSteps={getSteps().filter((s) => parseInt(s.id) < parseInt(toStep.id))}
toSteps={getSteps().filter((s) => parseInt(s.id) > parseInt(fromStep.id))}
/>
{changedFiles.length > 0 && (
<FileNavDropdown
files={changedFiles.map((file) => ({
+3 -3
View File
@@ -5,7 +5,7 @@ import { ChevronLeft, ChevronRight } from 'lucide-react'
import { getStep, getSteps } from '@/lib/steps'
import { ReadmeRenderer } from '@/components/readme-renderer'
import { DiffSelector } from '@/components/diff-selector'
import { StepDiffSelector } from '@/components/step-diff-selector'
import {
Breadcrumb,
BreadcrumbItem,
@@ -60,7 +60,7 @@ export default async function StepPage({ params }: PageProps) {
const nextStep = currentIndex < steps.length - 1 ? steps[currentIndex + 1] : null
// Steps available for diff comparison (excluding current step)
const diffTargets = steps.filter((s) => s.id !== step.id)
const diffTargets = steps.filter((s) => parseInt(s.id) > parseInt(step.id))
return (
<div className="container py-8">
@@ -94,7 +94,7 @@ export default async function StepPage({ params }: PageProps) {
</H1>
{/* Diff action dropdown */}
<DiffSelector currentStepId={step.id} steps={diffTargets} />
<StepDiffSelector currentStepId={step.id} steps={diffTargets} />
</div>
{/* README Content */}
+39
View File
@@ -0,0 +1,39 @@
'use client'
import { useRouter } from 'next/navigation'
import { DiffSelector } from '@/components/diff-selector'
import type { Step } from '@/lib/steps'
interface DiffPageSelectorProps {
fromStep: Step
toStep: Step
fromSteps: Step[]
toSteps: Step[]
}
export function DiffPageSelector({
fromStep,
toStep,
fromSteps,
toSteps,
}: DiffPageSelectorProps) {
const router = useRouter()
return (
<div className="flex items-center gap-2">
<DiffSelector
steps={fromSteps}
value={fromStep.id}
placeholder="From step..."
onSelect={(newFromId) => router.push(`/steps/${newFromId}/diff/${toStep.id}`)}
/>
<span className="text-muted-foreground"></span>
<DiffSelector
steps={toSteps}
value={toStep.id}
placeholder="To step..."
onSelect={(newToId) => router.push(`/steps/${fromStep.id}/diff/${newToId}`)}
/>
</div>
)
}
+28 -14
View File
@@ -1,6 +1,5 @@
'use client'
import { useRouter } from 'next/navigation'
import {
Select,
SelectContent,
@@ -11,28 +10,43 @@ import {
import type { Step } from '@/lib/steps'
interface DiffSelectorProps {
currentStepId: string
steps: Step[]
value?: string
placeholder?: string
onSelect: (stepId: string) => void
}
export function DiffSelector({ currentStepId, steps }: DiffSelectorProps) {
const router = useRouter()
const handleValueChange = (value: string | null) => {
if (value) {
router.push(`/steps/${currentStepId}/diff/${value}`)
export function DiffSelector({
steps,
value,
placeholder = 'Compare with...',
onSelect,
}: DiffSelectorProps) {
const handleValueChange = (newValue: string | null) => {
if (newValue) {
onSelect(newValue)
}
}
const selectedStep = steps.find((s) => s.id === value)
return (
<Select onValueChange={handleValueChange}>
<SelectTrigger className="min-w-64">
<SelectValue placeholder="Compare with..." />
<Select value={value} onValueChange={handleValueChange}>
<SelectTrigger className="min-w-48">
<SelectValue placeholder={placeholder}>
{selectedStep && (
<>
<span className="font-mono text-xs">{selectedStep.id}:</span>{' '}
<span className="text-xs">{selectedStep.title}</span>
</>
)}
</SelectValue>
</SelectTrigger>
<SelectContent>
{steps.map((target) => (
<SelectItem key={target.id} value={target.id}>
Step {target.id}: {target.title}
{steps.map((step) => (
<SelectItem key={step.id} value={step.id}>
<span className="font-mono text-xs">{step.id}:</span>{' '}
<span className="text-xs">{step.title}</span>
</SelectItem>
))}
</SelectContent>
+21
View File
@@ -0,0 +1,21 @@
'use client'
import { useRouter } from 'next/navigation'
import { DiffSelector } from '@/components/diff-selector'
import type { Step } from '@/lib/steps'
interface StepDiffSelectorProps {
currentStepId: string
steps: Step[]
}
export function StepDiffSelector({ currentStepId, steps }: StepDiffSelectorProps) {
const router = useRouter()
return (
<DiffSelector
steps={steps}
onSelect={(targetId) => router.push(`/steps/${currentStepId}/diff/${targetId}`)}
/>
)
}