From 997ba31e1ad6f35e02b139fa1ad7738d35948eae Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 13 Sep 2026 23:54:09 +0000 Subject: [PATCH] Add interactive check-in calendar --- app/checkin-client.tsx | 11 ++++++----- app/globals.css | 1 + 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/app/checkin-client.tsx b/app/checkin-client.tsx index 778af6b..f091913 100644 --- a/app/checkin-client.tsx +++ b/app/checkin-client.tsx @@ -1,13 +1,14 @@ "use client"; import {useEffect,useMemo,useState} from "react"; -import {CalendarDays,CheckCircle2,Cloud,Flame,ChevronLeft,ChevronRight} from "lucide-react"; +import {CalendarDays,CheckCircle2,Cloud,ChevronLeft,ChevronRight} from "lucide-react"; import {Checkbox} from "@/components/ui/checkbox"; import {Progress} from "@/components/ui/progress"; import {daysBetween,plan} from "./plan"; -type Record={doneTasks:string;mood:string;note:string}; +type Rec={doneTasks:string;mood:string;note:string}; const fmt=(s:string)=>new Intl.DateTimeFormat("zh-CN",{month:"long",day:"numeric",weekday:"short",timeZone:"UTC"}).format(new Date(s+"T00:00:00Z")); -export default function CheckinClient(){const [selected,setSelected]=useState(Math.max(0,Math.min(89,daysBetween("2026-09-13"))));const [records,setRecords]=useState>({});const [status,setStatus]=useState("正在读取云端进度…");const [saving,setSaving]=useState(false);const day=plan[selected],record=records[day.date]||{doneTasks:"[]",mood:"",note:""};const done=useMemo(()=>{try{return JSON.parse(record.doneTasks)as string[]}catch{return[]}},[record.doneTasks]);const total=plan.reduce((n,p)=>n+p.tasks.length,0);const finished=Object.values(records).reduce((n,r)=>{try{return n+JSON.parse(r.doneTasks).length}catch{return n}},0);const completeDays=plan.filter(p=>{try{return JSON.parse(records[p.date]?.doneTasks||"[]").length===p.tasks.length}catch{return false}}).length;useEffect(()=>{fetch("/api/progress").then(async r=>{const x=await r.json();if(!r.ok)throw Error(x.error);const next:Record={};x.progress.forEach((p:Record&{planDate:string})=>next[p.planDate]=p);setRecords(next);setStatus("云端同步已开启");}).catch(e=>setStatus(e.message));},[]); -async function save(next:Record){setRecords(x=>({...x,[day.date]:next}));setSaving(true);try{const r=await fetch("/api/progress",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({date:day.date,doneTasks:JSON.parse(next.doneTasks),mood:next.mood,note:next.note})});const x=await r.json();if(!r.ok)throw Error(x.error);setRecords(v=>({...v,[day.date]:x.progress}));setStatus("已保存到云端");}catch(e){setStatus(e instanceof Error?e.message:"保存失败");}finally{setSaving(false);}} +export default function CheckinClient(){const [selected,setSelected]=useState(Math.max(0,Math.min(89,daysBetween("2026-09-13"))));const [month,setMonth]=useState(8);const [records,setRecords]=useState>({});const [status,setStatus]=useState("正在读取云端进度…");const [saving,setSaving]=useState(false);const day=plan[selected],record=records[day.date]||{doneTasks:"[]",mood:"",note:""};const done=useMemo(()=>{try{return JSON.parse(record.doneTasks)as string[]}catch{return[]}},[record.doneTasks]);const total=plan.reduce((n,p)=>n+p.tasks.length,0),finished=Object.values(records).reduce((n,r)=>{try{return n+JSON.parse(r.doneTasks).length}catch{return n}},0);const complete=plan.filter(p=>{try{return JSON.parse(records[p.date]?.doneTasks||"[]").length===p.tasks.length}catch{return false}}).length;const blanks=Array(new Date(Date.UTC(2026,month,1)).getUTCDay()).fill(null),length=new Date(Date.UTC(2026,month+1,0)).getUTCDate();const dates=Array.from({length},(_,i)=>"2026-"+String(month+1).padStart(2,"0")+"-"+String(i+1).padStart(2,"0")); +useEffect(()=>{fetch("/api/progress").then(async r=>{const x=await r.json();if(!r.ok)throw Error(x.error);const all:Record={};x.progress.forEach((p:Rec&{planDate:string})=>all[p.planDate]=p);setRecords(all);setStatus("云端同步已开启");}).catch(e=>setStatus(e.message));},[]); +async function save(next:Rec){setRecords(v=>({...v,[day.date]:next}));setSaving(true);try{const r=await fetch("/api/progress",{method:"PUT",headers:{"Content-Type":"application/json"},body:JSON.stringify({date:day.date,doneTasks:JSON.parse(next.doneTasks),mood:next.mood,note:next.note})}),x=await r.json();if(!r.ok)throw Error(x.error);setRecords(v=>({...v,[day.date]:x.progress}));setStatus("已保存到云端");}catch(e){setStatus(e instanceof Error?e.message:"保存失败");}finally{setSaving(false)}} const toggle=(key:string,on:boolean)=>void save({...record,doneTasks:JSON.stringify(on?[...done,key]:done.filter(x=>x!==key))}); -return

CET-6 · 2026

大学英语六级 · 90天打卡

从今天的一件事开始,完成比完美重要。

{saving?"正在云端保存…":status}

} value={Math.round(finished/total*100)+"%"} label="总体完成度" detail={finished+" / "+total+" 项任务"}/>} value={completeDays+" 天"} label="完整完成" detail="目标共 90 天"/>} value={completeDays?"保持中":"0 天"} label="连续打卡" detail="按自己的节奏来"/>} value={day.phase} label="当前阶段" detail="12 月 12 日笔试"/>

{day.phase} · {day.week}

{fmt(day.date)}

已完成 {done.length} / {day.tasks.length} 项
{day.tasks.map(t=>)}

今日状态

{["专注","平稳","有点累","需要调整"].map(m=>)}