wip: sadhna streak
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* /sadhna-streak/preview — DEV-ONLY sandbox for the sādhana creeper.
|
||||
*
|
||||
* Lets you fast-track the vine: scrub the streak length, set today's
|
||||
* in-progress rounds/posts, and dial the health (growth) score to watch the
|
||||
* vine climb the pole, sprout folds + blossoms, and wither — all without
|
||||
* seeding real logs. Returns nothing in production builds.
|
||||
*
|
||||
* The synthesized payload matches what the BFF's Dashboard builds, so what
|
||||
* you see here mirrors live.
|
||||
*/
|
||||
|
||||
import { useState } from "react";
|
||||
import { SadhnaVine } from "@/components/sadhna/sadhna-vine";
|
||||
import type { VineDay } from "@/lib/sadhna";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
export default function SadhnaPreviewPage() {
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
return (
|
||||
<main className="flex-1 flex items-center justify-center px-4 py-12">
|
||||
<p className="text-sm text-muted-foreground">Not available.</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
return <Preview />;
|
||||
}
|
||||
|
||||
/** Build a vine_days payload exactly the way the BFF would. */
|
||||
function buildDays(
|
||||
completed: number,
|
||||
target: number,
|
||||
todayRounds: number,
|
||||
todayPosts: number,
|
||||
todayComplete: boolean,
|
||||
): VineDay[] {
|
||||
const out: VineDay[] = [];
|
||||
const now = new Date();
|
||||
const todayMs = Date.UTC(
|
||||
now.getUTCFullYear(),
|
||||
now.getUTCMonth(),
|
||||
now.getUTCDate(),
|
||||
);
|
||||
const endOffset = todayComplete ? 0 : 1; // days-ago of the last finished fold
|
||||
for (let i = completed - 1; i >= 0; i--) {
|
||||
const daysAgo = endOffset + i;
|
||||
const date = new Date(todayMs - daysAgo * 86400000)
|
||||
.toISOString()
|
||||
.slice(0, 10);
|
||||
out.push({
|
||||
date,
|
||||
rounds: target + (i % 3),
|
||||
posts: 1 + (i % 2),
|
||||
completed: true,
|
||||
in_progress: false,
|
||||
});
|
||||
}
|
||||
if (!todayComplete) {
|
||||
out.push({
|
||||
date: new Date(todayMs).toISOString().slice(0, 10),
|
||||
rounds: todayRounds,
|
||||
posts: todayPosts,
|
||||
completed: false,
|
||||
in_progress: true,
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function Preview() {
|
||||
const [completed, setCompleted] = useState(3);
|
||||
const [target] = useState(16);
|
||||
const [todayComplete, setTodayComplete] = useState(false);
|
||||
const [todayRounds, setTodayRounds] = useState(8);
|
||||
const [todayPosts, setTodayPosts] = useState(0);
|
||||
const [growth, setGrowth] = useState(60);
|
||||
|
||||
const days = buildDays(completed, target, todayRounds, todayPosts, todayComplete);
|
||||
const streak = completed; // current streak = finished folds
|
||||
const alive = growth > 0;
|
||||
|
||||
return (
|
||||
<main className="flex-1 w-full max-w-2xl mx-auto px-4 sm:px-6 py-5 flex flex-col gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="rounded bg-amber-500/15 px-2 py-0.5 text-[11px] font-semibold uppercase tracking-wide text-amber-600 dark:text-amber-400">
|
||||
Dev
|
||||
</span>
|
||||
<h1 className="font-[family-name:var(--font-title)] text-xl font-semibold text-foreground">
|
||||
Vine preview
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Controls */}
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-x-6 gap-y-3 rounded-2xl border border-border bg-card p-4">
|
||||
<Slider
|
||||
label="Streak (finished folds)"
|
||||
value={completed}
|
||||
min={0}
|
||||
max={30}
|
||||
onChange={setCompleted}
|
||||
/>
|
||||
<Slider
|
||||
label="Health (growth)"
|
||||
value={growth}
|
||||
min={0}
|
||||
max={100}
|
||||
onChange={setGrowth}
|
||||
/>
|
||||
<Slider
|
||||
label="Today's rounds"
|
||||
value={todayRounds}
|
||||
min={0}
|
||||
max={target}
|
||||
disabled={todayComplete}
|
||||
onChange={setTodayRounds}
|
||||
/>
|
||||
<Slider
|
||||
label="Today's posts"
|
||||
value={todayPosts}
|
||||
min={0}
|
||||
max={3}
|
||||
disabled={todayComplete}
|
||||
onChange={setTodayPosts}
|
||||
/>
|
||||
<label className="flex items-center gap-2 text-sm text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={todayComplete}
|
||||
onChange={(e) => setTodayComplete(e.target.checked)}
|
||||
className="size-4 accent-primary"
|
||||
/>
|
||||
Today already complete
|
||||
</label>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<Chip onClick={() => setGrowth(Math.min(100, completed * 4))}>
|
||||
sync health → streak
|
||||
</Chip>
|
||||
<Chip onClick={() => setGrowth(0)}>kill</Chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Live vine — same panel chrome as the real page */}
|
||||
<section className="relative h-[68svh] min-h-[420px] rounded-2xl border border-border bg-gradient-to-b from-sky-50/50 to-emerald-50/30 dark:from-sky-950/20 dark:to-emerald-950/10 overflow-hidden">
|
||||
<SadhnaVine
|
||||
days={days}
|
||||
targetRounds={target}
|
||||
growth={growth}
|
||||
alive={alive}
|
||||
totalStreak={streak}
|
||||
/>
|
||||
</section>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function Slider({
|
||||
label,
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
min: number;
|
||||
max: number;
|
||||
onChange: (v: number) => void;
|
||||
disabled?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className={cn("space-y-1", disabled && "opacity-40")}>
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted-foreground">{label}</span>
|
||||
<span className="font-semibold tabular-nums text-foreground">{value}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
min={min}
|
||||
max={max}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
onChange={(e) => onChange(Number(e.target.value))}
|
||||
className="w-full accent-primary"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Chip({
|
||||
children,
|
||||
onClick,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
onClick: () => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={onClick}
|
||||
className="rounded-full border border-border px-3 py-1 text-xs text-muted-foreground transition-colors hover:bg-muted/50 hover:text-foreground"
|
||||
>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user