Skip to content

Somesh-S-Dev/Taskr

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

◈ Taskr

Minimal, offline-first task & reminder app
Web · Mobile · Desktop — one codebase to rule them all


Table of Contents

  1. Project Overview
  2. Project Structure
  3. Quick Start (Web)
  4. Feature Guide
  5. Keyboard Shortcuts
  6. Data Model
  7. Platform Guides
  8. Cloud Sync (Supabase)
  9. Google Calendar Integration
  10. Push Notifications (Web)
  11. Deployment
  12. Customisation
  13. Roadmap
  14. Tech Stack Summary

1. Project Overview

Taskr is a distraction-free productivity tool built around three principles:

Principle Implementation
⚡ Fast Open → add task in < 2 sec via Quick Add bar
📴 Offline All data stored in localStorage — works with no internet
🧩 Minimal Only what matters: tasks, priorities, reminders, calendar

What's included out of the box

  • ✅ Full task CRUD (create / read / update / delete)
  • ⏰ Due dates & times
  • 🔁 Recurring tasks (daily / weekly / monthly)
  • 📌 Pin important tasks to the top
  • 🎯 Priority levels (High / Medium / Low) with colour coding
  • 📅 Monthly calendar view with per-day task density
  • 🔍 Full-text search across title and notes
  • 📊 Today's progress bar
  • ⚠️ Overdue task warning banner
  • 🌙 Dark / light mode (persisted)
  • 📱 Responsive layout — sidebar on desktop, tab bar on mobile
  • 💾 Offline-first via localStorage

2. Project Structure

taskr/
├── index.html                  # App shell
├── vite.config.js              # Vite bundler config
├── package.json
├── .gitignore
├── public/
│   └── manifest.json           # PWA manifest
└── src/
    ├── main.jsx                # React entry point
    ├── App.jsx                 # Root component, state, layout
    ├── hooks/
    │   └── useLocalStorage.js  # Persistent state hook
    ├── utils/
    │   ├── helpers.js          # uid(), todayISO(), relDate(), …
    │   └── constants.js        # Priority config, demo data, nav items
    └── components/
        ├── TaskItem.jsx        # Single task row + context menu
        ├── TaskModal.jsx       # Add / edit modal
        ├── CalendarView.jsx    # Monthly calendar + day detail
        ├── Sidebar.jsx         # Desktop sidebar + mobile tab bar
        ├── QuickAdd.jsx        # Inline quick-add strip
        └── EmptyState.jsx      # Empty-view illustration

3. Quick Start (Web)

Prerequisites

Tool Minimum version Install
Node.js 18.x https://nodejs.org or nvm install 18
npm 9.x Bundled with Node

Steps

# 1 — Clone / unzip the project
cd taskr

# 2 — Install dependencies (takes ~30 seconds)
npm install

# 3 — Start the dev server
npm run dev

The app opens automatically at http://localhost:5173

Build for production

npm run build        # outputs to dist/
npm run preview      # serve the production build locally

4. Feature Guide

Adding tasks

Quick Add (fastest path)
Type in the bar below the header → press Enter.
Creates a Medium-priority task due today.

Full modal
Click + New Task button (or press ⌘N / Ctrl+N).
Fill in title, notes, date, time, priority, recurrence, and pin toggle.
Press ⌘↩ to save or Esc to close.

Filters / views

View Shows
Today Incomplete tasks due today
Upcoming Incomplete tasks due after today
All Tasks All incomplete tasks
Calendar Monthly grid — click a day to inspect tasks
Completed All completed tasks

Priorities

Priority Colour Use for
High Red Blocking / urgent tasks
Medium Orange Normal work
Low Green Nice-to-have, housekeeping

Recurring tasks

Set repeat to Daily / Weekly / Monthly in the modal.
The badge appears on the task row.

Note: auto-rescheduling on completion is on the roadmap (see §13).

Pinning

Pin a task via the ⋯ context menu → Pin to top.
Pinned tasks always appear first, grouped separately.


5. Keyboard Shortcuts

Shortcut Action
⌘N / Ctrl+N Open new-task modal
⌘↩ / Ctrl+↩ Save modal form
Esc Close modal
Enter Submit Quick Add

6. Data Model

Tasks are stored as JSON in localStorage under the key taskr_v2_tasks.

interface Task {
  id:          string;             // "t_<timestamp>_<random>"
  title:       string;             // required
  notes:       string;             // optional, free text
  dueDate:     string;             // ISO date "YYYY-MM-DD"
  dueTime:     string;             // "HH:MM" or ""
  priority:    "high" | "medium" | "low";
  isCompleted: boolean;
  isPinned:    boolean;
  recurring:   "none" | "daily" | "weekly" | "monthly";
  createdAt:   string;             // ISO datetime
}

Additional persisted keys:

Key Value
taskr_v2_filter active view filter
taskr_v2_dark boolean (dark mode)

7. Platform Guides

7A. Desktop App (Electron)

Wrap the web build in Electron to get a native desktop window with system-tray support.

Install Electron

npm install --save-dev electron electron-builder concurrently wait-on

Create electron/main.js

const { app, BrowserWindow, Tray, Menu, nativeImage } = require("electron");
const path = require("path");

let win, tray;

function createWindow() {
  win = new BrowserWindow({
    width: 1200,
    height: 780,
    minWidth: 420,
    minHeight: 500,
    titleBarStyle: "hiddenInset",   // macOS native look
    webPreferences: { nodeIntegration: false, contextIsolation: true },
    icon: path.join(__dirname, "icon.png"),
  });

  // In dev, load Vite's dev server; in production, load built files
  const isDev = process.env.NODE_ENV !== "production";
  if (isDev) {
    win.loadURL("http://localhost:5173");
    win.webContents.openDevTools();
  } else {
    win.loadFile(path.join(__dirname, "../dist/index.html"));
  }
}

function createTray() {
  const icon = nativeImage.createFromPath(path.join(__dirname, "tray-icon.png"));
  tray = new Tray(icon.resize({ width: 16 }));
  const menu = Menu.buildFromTemplate([
    { label: "Open Taskr", click: () => win.show() },
    { label: "New Task",   click: () => { win.show(); win.webContents.send("new-task"); } },
    { type: "separator" },
    { label: "Quit",       click: () => app.quit() },
  ]);
  tray.setContextMenu(menu);
  tray.setToolTip("Taskr");
}

app.whenReady().then(() => {
  createWindow();
  createTray();
});

app.on("window-all-closed", () => {
  if (process.platform !== "darwin") app.quit();
});

app.on("activate", () => {
  if (BrowserWindow.getAllWindows().length === 0) createWindow();
});

Add scripts to package.json

"scripts": {
  "electron:dev":   "concurrently \"npm run dev\" \"wait-on http://localhost:5173 && electron electron/main.js\"",
  "electron:build": "npm run build && electron-builder"
},
"build": {
  "appId": "com.yourname.taskr",
  "productName": "Taskr",
  "directories": { "output": "electron-dist" },
  "files": ["dist/**/*", "electron/**/*"],
  "mac":  { "category": "public.app-category.productivity" },
  "win":  { "target": "nsis" },
  "linux":{ "target": "AppImage" }
}

Run

npm run electron:dev      # development
npm run electron:build    # package for distribution

7B. Mobile App (React Native)

The logic (data model, hooks, utils) is identical. Only the UI components are rewritten using React Native primitives.

Setup

npx create-expo-app taskr-mobile --template blank
cd taskr-mobile
npx expo install @react-native-async-storage/async-storage
npx expo install expo-notifications
npx expo install expo-calendar

Copy shared logic

Copy these files verbatim from the web project — they have zero browser dependencies:

src/utils/helpers.js    → taskr-mobile/src/utils/helpers.js
src/utils/constants.js  → taskr-mobile/src/utils/constants.js

Replace useLocalStorage with useAsyncStorage

// src/hooks/useAsyncStorage.js
import { useState, useEffect } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";

export function useAsyncStorage(key, initial) {
  const [value, setValue] = useState(initial);

  useEffect(() => {
    AsyncStorage.getItem(key)
      .then((raw) => raw !== null && setValue(JSON.parse(raw)))
      .catch(() => {});
  }, [key]);

  useEffect(() => {
    AsyncStorage.setItem(key, JSON.stringify(value)).catch(() => {});
  }, [key, value]);

  return [value, setValue];
}

Map web components → React Native

Web component React Native equivalent
<div> <View>
<p>, <span> <Text>
<button> <TouchableOpacity> or <Pressable>
<input> <TextInput>
<textarea> <TextInput multiline>
<ul> / list <FlatList> (virtualised)

Push notifications (Expo)

import * as Notifications from "expo-notifications";

// Request permission on first launch
async function requestPermission() {
  const { status } = await Notifications.requestPermissionsAsync();
  return status === "granted";
}

// Schedule a reminder for a task
async function scheduleReminder(task) {
  if (!task.dueDate || !task.dueTime) return;
  const [h, m] = task.dueTime.split(":").map(Number);
  const trigger = new Date(task.dueDate + "T" + task.dueTime);
  trigger.setMinutes(trigger.getMinutes() - 15); // 15 min early

  await Notifications.scheduleNotificationAsync({
    content: {
      title: "◈ Taskr Reminder",
      body:  task.title,
      data:  { taskId: task.id },
    },
    trigger,
  });
}

Run on device

npx expo start          # scan QR code with Expo Go
npx expo run:ios        # requires macOS + Xcode
npx expo run:android    # requires Android Studio

8. Cloud Sync (Supabase)

Supabase gives you a Postgres database + real-time subscriptions + auth — all free up to 500MB.

Setup

  1. Create a project at https://supabase.com
  2. Run this SQL in the Supabase SQL editor:
create table tasks (
  id          text primary key,
  user_id     uuid references auth.users not null,
  title       text not null,
  notes       text default '',
  due_date    date,
  due_time    text default '',
  priority    text default 'medium',
  is_completed boolean default false,
  is_pinned   boolean default false,
  recurring   text default 'none',
  created_at  timestamptz default now()
);

-- Row-level security: users see only their own tasks
alter table tasks enable row level security;
create policy "own tasks" on tasks
  for all using (auth.uid() = user_id);
  1. Install the client:
npm install @supabase/supabase-js
  1. Create src/lib/supabase.js:
import { createClient } from "@supabase/supabase-js";

export const supabase = createClient(
  import.meta.env.VITE_SUPABASE_URL,
  import.meta.env.VITE_SUPABASE_ANON_KEY
);
  1. Create .env.local (never commit this):
VITE_SUPABASE_URL=https://xxxx.supabase.co
VITE_SUPABASE_ANON_KEY=your-anon-key

Sync strategy

Replace useLocalStorage writes with Supabase upserts:

// Save task
async function saveTaskRemote(task) {
  const { data: { user } } = await supabase.auth.getUser();
  await supabase.from("tasks").upsert({
    ...task,
    user_id:      user.id,
    due_date:     task.dueDate   || null,
    due_time:     task.dueTime   || "",
    is_completed: task.isCompleted,
    is_pinned:    task.isPinned,
    created_at:   task.createdAt,
  });
}

// Load tasks on startup
async function loadTasksRemote() {
  const { data } = await supabase
    .from("tasks")
    .select("*")
    .order("created_at", { ascending: false });
  return data.map((row) => ({
    ...row,
    dueDate:     row.due_date     || "",
    dueTime:     row.due_time     || "",
    isCompleted: row.is_completed,
    isPinned:    row.is_pinned,
    createdAt:   row.created_at,
  }));
}

Offline-first pattern: keep localStorage as the primary source; sync to Supabase in the background. On app load, check network — if online, merge remote → local.


9. Google Calendar Integration

Sync tasks to Google Calendar as events so they appear alongside your schedule.

Setup

  1. Go to https://console.cloud.google.com
  2. Create a project → Enable Google Calendar API
  3. Create OAuth 2.0 credentials (Web application)
  4. Add http://localhost:5173 to Authorised JavaScript origins
npm install @react-oauth/google

Auth flow

// src/lib/googleAuth.js
const CLIENT_ID = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const SCOPE     = "https://www.googleapis.com/auth/calendar.events";

export async function signInGoogle() {
  return new Promise((resolve, reject) => {
    const client = window.google.accounts.oauth2.initTokenClient({
      client_id: CLIENT_ID,
      scope: SCOPE,
      callback: (resp) => resp.error ? reject(resp) : resolve(resp.access_token),
    });
    client.requestAccessToken();
  });
}

Push a task as a calendar event

export async function pushTaskToCalendar(task, accessToken) {
  if (!task.dueDate) return;

  const start = task.dueTime
    ? { dateTime: `${task.dueDate}T${task.dueTime}:00`, timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone }
    : { date: task.dueDate };

  const end = task.dueTime
    ? { dateTime: `${task.dueDate}T${task.dueTime.replace(/(\d+)/, (h) => String(+h + 1).padStart(2, "0"))}:00` }
    : { date: task.dueDate };

  const body = {
    summary:     task.title,
    description: task.notes || "",
    start, end,
    colorId:     task.priority === "high" ? "11" : task.priority === "medium" ? "5" : "2",
  };

  const resp = await fetch("https://www.googleapis.com/calendar/v3/calendars/primary/events", {
    method:  "POST",
    headers: { Authorization: `Bearer ${accessToken}`, "Content-Type": "application/json" },
    body:    JSON.stringify(body),
  });

  return resp.json();
}

Add VITE_GOOGLE_CLIENT_ID=your-client-id to .env.local.


10. Push Notifications (Web)

Web Push lets reminders fire even when the browser tab is closed.

Register a service worker

Create public/sw.js:

self.addEventListener("push", (event) => {
  const data = event.data?.json() ?? {};
  event.waitUntil(
    self.registration.showNotification(data.title || "Taskr Reminder", {
      body:    data.body || "",
      icon:    "/icon-192.png",
      badge:   "/icon-192.png",
      vibrate: [200, 100, 200],
    })
  );
});

Register in your app:

if ("serviceWorker" in navigator) {
  navigator.serviceWorker.register("/sw.js");
}

Schedule reminders

Use the Notifications API for near-future reminders (same tab / PWA):

async function scheduleWebReminder(task) {
  if (Notification.permission !== "granted") {
    await Notification.requestPermission();
  }
  const [h, m] = (task.dueTime || "09:00").split(":").map(Number);
  const fireAt = new Date(task.dueDate);
  fireAt.setHours(h, m - 15, 0, 0);   // 15 min before
  const delay = fireAt.getTime() - Date.now();
  if (delay < 0) return;

  setTimeout(() => {
    new Notification("◈ Taskr", { body: task.title, icon: "/icon-192.png" });
  }, delay);
}

11. Deployment

Vercel (recommended — free)

npm install -g vercel
vercel --prod

Netlify

npm run build
# Drag & drop the dist/ folder at netlify.com/drop
# — or —
npx netlify-cli deploy --prod --dir=dist

GitHub Pages

npm install --save-dev gh-pages

Add to package.json:

"homepage": "https://yourusername.github.io/taskr",
"scripts": {
  "deploy": "npm run build && gh-pages -d dist"
}
npm run deploy

Docker (self-hosted)

FROM node:20-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM nginx:alpine
COPY --from=build /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
docker build -t taskr .
docker run -p 8080:80 taskr

12. Customisation

Change accent colour

Edit src/utils/constants.jsPRIORITY_CONFIG.
The main blue accent (#4f8ef7) appears inline in App.jsx and component files — do a project-wide find & replace.

Change fonts

Edit the @import URL in the GLOBAL_CSS constant inside App.jsx. Pair any Google Fonts combination you like.

Add a new view / filter

  1. Add an entry to NAV_ITEMS in constants.js
  2. Add a label to FILTER_LABELS in constants.js
  3. Add a case in the switch block inside App.jsx's useMemo

Custom task fields

Add fields to the Task interface in the data model section above, extend the BLANK object in TaskModal.jsx, and add the corresponding <input> elements.


13. Roadmap

Planned features not yet implemented:

  • Auto-reschedule recurring tasks on completion
  • Drag-and-drop reordering
  • Sub-tasks / checklist items
  • Tags / labels
  • Pomodoro timer integration
  • Export to CSV / JSON
  • End-to-end encryption for cloud sync
  • Collaborative shared lists
  • Siri / Google Assistant integration (mobile)
  • Widget support (iOS / Android)

14. Tech Stack Summary

Layer Technology Notes
UI framework React 18 Hooks-only, no class components
Bundler Vite 5 Sub-second HMR
Styling Inline styles Zero CSS-in-JS dependencies
Fonts Bricolage Grotesque + JetBrains Mono Via Google Fonts CDN
Storage localStorage Offline-first, zero setup
Desktop Electron (optional) See §7A
Mobile React Native + Expo (optional) See §7B
Cloud sync Supabase (optional) Postgres + realtime + auth
Calendar sync Google Calendar API (optional) See §9
Notifications Web Push + Service Worker See §10
Deployment Vercel / Netlify / Docker See §11

Made with ◈ — keep it minimal, keep it fast.

About

Taskr is a distraction-free productivity tool built around three principles: Fast, Offline, Minimal

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors