Blog / Learning notes

Building an Offline Study Planner With No Backend

Cover image for Building an Offline Study Planner With No Backend

StudyPilot is a study manager I built that runs entirely in the browser. It has subjects, tasks, flashcards, a focus timer, analytics and a dashboard you can rearrange. It has no backend, no accounts and no AI API. The part I was most curious about was the planner, which answers one question: what should I study today? It does this with plain TypeScript rules, not a model.

Why no backend

I wanted StudyPilot to open instantly, work without a connection and not ask anyone to sign up. Without a server there is nothing to host but static files.

Storing everything in the browser

All app state lives in one Zustand store wrapped in Zustand's persist middleware, which saves to localStorage under the key studypilot:v1. A partialize function picks what gets saved: subjects, tasks, cards, sessions, notes, settings, dashboard widgets and the timer.

Two details mattered:

  • Hydration is manual. The store uses skipHydration: true, and the app shell calls useStore.persist.rehydrate() after it mounts. Next.js renders the page before browser storage is available, so the saved data is loaded only once the app is running in the browser.
  • Backups are plain JSON. The only copy of your data is in one browser, so the settings page can download a studypilot-backup-YYYY-MM-DD.json file and import it again.

On first launch the store is filled with demo data, which you can clear with one click.

A dashboard you can customise

The dashboard is made of widgets: today's plan, tasks, subjects, focus timer, study hours, deadlines and exams, streak, quick notes, flashcards due and weekly goal. You can drag them into a new order (with dnd-kit), add or remove them, and resize each one from 1 to 4 columns wide and 1 to 3 rows tall. Each widget is stored as a small config object with an id, type, width and height, in the same persisted store as everything else. So the layout is saved like your tasks and is included in backups.

The rule-based planner

All the planner logic is in src/lib/planner.ts. The functions are pure: same data in, same plan out. This made them easy to test with Vitest.

Scoring a task

Every open task gets a score. Higher means "do this sooner":

export function taskScore(task: Task, subjects: Subject[], today: string): number {
  let s = PRIORITY_WEIGHT[task.priority] * 10;
  if (task.due) {
    const d = diffDays(today, task.due);
    if (d < 0) s += 50 + Math.min(-d, 7) * 3;
    else s += Math.max(0, 30 - d * 4);
  } else s -= 5;
  const subj = subjects.find((x) => x.id === task.subjectId);
  if (subj?.examDate) {
    const e = diffDays(today, subj.examDate);
    if (e >= 0 && e <= 14) s += (14 - e) * 1.5;
  }
  if (subj) s += (subj.difficulty - 1) * 2;
  if (task.status === "doing") s += 8;
  if (task.scheduledFor === today) s += 12;
  else if (task.scheduledFor && task.scheduledFor > today) s -= 15;
  return Math.round(s * 10) / 10;
}

Priority sets the base. Overdue work gets a large boost, and nearby deadlines get a smaller one. An exam within 14 days, a harder subject or a task already in progress pushes the score up. A task scheduled for a later day is pushed down. A matching taskReason function turns the same signals into a label like "Due tomorrow · high priority".

Building today's plan

buildDailyPlan turns those scores into a timed schedule. Your capacity is your daily study hours minus what you've already logged today. Then it fills time in this order:

  1. Flashcard warm-up. If cards are due, it adds a review block of 5 to 30 minutes, based on how many cards are due.
  2. Tasks by score. Each task gets a block of up to 90 minutes. Longer tasks get a "(part)" block. Anything that doesn't fit goes into an overflow list instead of being dropped.
  3. Subject sessions. If at least 20 minutes are left, up to two subjects that are behind their weekly target or have an exam coming up get an active-recall session of up to 45 minutes.

After any block of 25 minutes or more, a break is added (if breaks are turned on). The plan never runs past midnight, so if you start late you get a shorter plan, not an impossible one.

Looking ahead 14 days

autoSchedule plans the next two weeks, earliest deadline first. Each task goes on the first day before its deadline that still has room. If no day has room, it goes on the least-loaded day and is flagged "at risk". Tasks without a deadline are spread over the least-busy days of the coming week. The assistant panel uses these results for its suggestions, such as rescheduling overdue work or warning that a deadline is at risk, and each suggestion comes with a one-click action.

What I learned

Not every "smart" feature needs a model. A clear scoring function and a few ordered rules did the job here, and since every rule is in one file, anyone can read why the planner suggested something. Keeping the logic pure also made the tests simple to write. One checks that it "never plans past midnight".


You can try StudyPilot at study-manager-fawn.vercel.app, and the source is on GitHub at mfaysal-dev/study-manager.

// note

How to read this note.

This is a learning note from studying the web. It is one small topic, written so I can remember it. It is not a course and not a claim that I have finished the subject.

If a sentence is wrong, say so from the contact page and name this title. Drafts never appear here. Related notes, when they exist, are other published posts, and the same sample rule applies to each of them.

Related notes

Sample note: what I am using PHP for
Learning notes Sample

Sample note: what I am using PHP for

A sample article for the blog layout. It is a practice note, not a finished tutorial or a claim of expertise.

September 20, 2026 · 2 min read