A personal task management app (think to-do lists) built with Laravel (API) and React + Vite (frontend). Authentication is handled with Laravel Sanctum using Bearer tokens.
Users can create, complete, edit, and delete tasks. Tasks may be optionally grouped into projects. Users can view the progress of each project based on percentage of project tasks completed. Users can only view tasks belonging to them.
- Backend: PHP 8.2+, Laravel 10+, Sanctum
- Frontend: React 18, Vite, Axios, React Router, Typescript, TailwindCSS (CDN)
- DB: MySQL/MariaDB (or SQLite)
- Auth: Sanctum Personal Access Tokens (Bearer)
- PHP 8.2+, Composer 2.x
- Node 18+ with npm/yarn/pnpm
- MySQL/MariaDB or SQLite
- Git
task-manager-app/
├─ backend/ # Laravel API
│ ├─ app/
│ ├─ database/
│ ├─ routes/api.php
│ └─ ...
└─ frontend/ # React + Vite client
├─ src/
├─ index.html
└─ ...
- Install & configure
git clone https://github.com/kelly-eugenia/task-manager-app.git
cd task-manager-app/backend
composer install
cp .env.example .env- Configure
.env
APP_NAME=Laravel
APP_ENV=local
APP_KEY= # will be generated
APP_DEBUG=true
APP_URL=http://localhost
# IMPORTANT: set app timezone for "today/overdue" to match local day
APP_TIMEZONE=Australia/Melbourne
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=task-manager
DB_USERNAME=root
DB_PASSWORD=Verify that timezone config is read from env in config/app.php:
'timezone' => env('APP_TIMEZONE', 'UTC'),Then clear caches:
php artisan config:clear
php artisan cache:clear- Configure
CORS
In config/cors.php, allow:
'allowed_origins' => [
'http://localhost:5173',
'http://127.0.0.1:5173',
'http://localhost:8000',
'http://127.0.0.1:8000',
],
'supports_credentials' => false, // fine for Bearer tokens- Generate key, migrate, seed
php artisan key:generate
php artisan migrate --seedThis seeds one demo user (with some tasks and projects):
- Email: test@example.com
- Password: testPassword
- Run the API
php artisan serve
# -> http://127.0.0.1:8000- Install & configure
cd ../frontend
npm install # or yarn / pnpm
cp .env.example .env- Set API base URL
VITE_API_BASE_URL=http://127.0.0.1:8000/api- Run the dev server
npm run dev # or yarn dev / pnpm dev
# -> http://127.0.0.1:5173src/contexts/AuthContext.tsx
- Holds
user,token, and areadyflag - Bootstraps auth on app load:
- Reads
auth_token/auth_userfromlocalStorage - If a token exists, calls
GET /api/userto validate and refresh the user - On failure (expired/invalid), clears storage and resets auth state
- Reads
- Exposes
login,register,logout- On success: saves
{ user, token }to state +localStorage, navigate to/ - On logout: best-effort
/api/logout, then clear storage and navigate to/login
- On success: saves
Global 401 handling:
If any request returns 401, a response interceptor logs the user out and redirects to /login automatically,
src/api.ts
- Central Axios instance with
baseURLfrom.env(VITE_API_BASE_URL) - Request interceptor injects
Authorization: Bearer <token>fromlocalStorage(if present)
Protected routes
- App routes are wrapped by a
ProtectedRoutethat checkstokenIf absent → redirect to/login
All protected routes require Authorization: Bearer <token>.
-
POST
/api/registerBody:
{ "name": "string", "email": "string", "password": "string", "password_confirmation": "string" }Returns:
{ "token": "...", "user": { ... } } -
POST
/api/loginBody:
{ "email": "string", "password": "string" }Returns:
{ "token": "...", "user": { ... } } -
POST
/api/logout(auth required) Revokes/Deletes the current token. -
GET
/api/user(auth required) Returns the authenticated user
(auth required)
-
GET
/api/tasksList tasks for the user.Query params
-
project_id— only tasks in that project -
status=completed— only completed tasks -
due=today | scheduled | overduetoday: tasks withdue_dateequal to todayscheduled: tasks with a non-nulldue_dateoverdue: non-completed tasks withdue_datebefore today
-
sort=due_date | created_at | priority(defaultdue_date) -
dir=asc | desc(defaultasc)
Ordering rules
- Incomplete tasks are always shown first
- When sorting by
due_date,NULLdue dates are pushed to the bottom - Priority order (ascending):
null < low < medium < high
-
-
POST
/api/tasksCreate task for the user. Body:{ "title": "required|string|max:100", "description": "string|null|max:255", "due_date": "YYYY-MM-DD|null", "priority": "low|medium|high|null", "project_id": "number|null" } -
GET
/api/tasks/{id}Show task (ownership enforced). -
PUT
/api/tasks/{id}Update task. Ifis_completedis toggled:false → true→ backend setscompleted_at = now()true → false→ backend clearscompleted_at
-
DELETE
/api/tasks/{id}Delete task.
(auth required)
-
GET
/api/projectsList user’s projects. Each includes:tasks_countcompleted_tasks_countprogress(computed)
-
POST
/api/projectsCreate project for the user. Body:{ "title": "required|string|max:100", "description": "required|string|max:255", "end_date": "YYYY-MM-DD|null" } -
GET
/api/projects/{id}Show project, with counts & computedprogress. Also retrieve tasks under the project, ordered by due date then created_at (can be filtered and sorted too). -
PUT
/api/projects/{id}Update project:title,description,end_date. -
DELETE
/api/projects/{id}Deletes the project and all its tasks (viacascadeOnDelete).
- Auth: Sanctum Personal Access Tokens; frontend stores token in
localStorageand sendsAuthorization: Bearer <token>. Logout revokes the current token. - Ownership: Every read/write query is scoped by
user_id(server-side). Tasks and projects can only belong or assigned to one user -- no other users can access them. - Dates:
due_dateandend_dateare date-only (YYYY-MM-DD). Overdue/today use date comparisons at local midnight. The UI uses helpers insrc/utils/dates.tsto normalize values for<input type="date">, format display, and compute “today/overdue” using the configured app timezone. - Projects/Tasks:
project_idon tasks is nullable, but the FK is configured withcascadeOnDelete(). Deleting a project will delete all its tasks. - Computed:
completed_atis managed by backend whenis_completedchanges. Projectprogressis computed from counts (not stored).
Running:
php artisan migrate:fresh --seedcreates one demo user with several projects and tasks:
- Email:
test@example.com - Password:
testPassword
Login example:
curl -X POST http://127.0.0.1:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"testPassword"}'Register
curl -X POST http://127.0.0.1:8000/api/register \
-H "Content-Type: application/json" \
-d '{"name":"John Smith","email":"john@example.com","password":"secret1234","password_confirmation":"secret1234"}'Login
curl -X POST http://127.0.0.1:8000/api/login \
-H "Content-Type: application/json" \
-d '{"email":"test@example.com","password":"testPassword"}'List project tasks
curl "http://127.0.0.1:8000/api/projects/1" \
-H "Authorization: Bearer <TOKEN>"Create a task
curl -X POST http://127.0.0.1:8000/api/tasks \
-H "Authorization: Bearer <TOKEN>" \
-H "Content-Type: application/json" \
-d '{"title":"Buy milk","due_date":"2025-10-20","priority":"medium","project_id":null}'