Skip to content

henriquesss/fairtask

Repository files navigation

FairTime

Server-authoritative timed tasks — A web application for conducting fair, tamper-proof timed assessments where all timing logic runs on the server.

Next.js Supabase TypeScript

Features

  • ⏱️ Server-Authoritative Timing — All time calculations happen on the server, preventing client-side manipulation
  • 🔄 Session Resumption — Users can close the browser and return; their timer continues
  • 📊 Admin Dashboard — View all submissions with status, timing, and proof URLs
  • 🎨 Modern UI — Dark theme with glass-morphism effects and smooth animations
  • Accessible — ARIA labels, keyboard navigation, reduced motion support

Tech Stack

  • Framework: Next.js 16 (App Router)
  • Database: Supabase (PostgreSQL)
  • Styling: Tailwind CSS 4
  • Language: TypeScript

Quick Start

1. Clone and Install

git clone <your-repo-url>
cd fairtask
npm install

2. Set Up Supabase

Follow the Supabase Setup Guide below to get your environment variables.

3. Configure Environment

Create a .env.local file in the project root:

NEXT_PUBLIC_SUPABASE_URL=your_project_url
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key

4. Run the Database Schema

Copy and run the SQL schema in your Supabase SQL Editor. See Database Schema below.

5. Start Development Server

npm run dev

Open http://localhost:3000 to see the app.


Supabase Setup Guide

Step 1: Create a Supabase Project

  1. Go to supabase.com and sign in (or create an account)
  2. Click "New Project"
  3. Fill in the details:
    • Name: fairtime (or any name you prefer)
    • Database Password: Generate a strong password (save this!)
    • Region: Choose the closest to your users
  4. Click "Create new project" and wait for it to initialize (~2 minutes)

Step 2: Get Your Environment Variables

Once your project is ready:

  1. Click "Project Settings" (gear icon) in the left sidebar
  2. Click "API" under Configuration

You'll see a page with your API credentials:

NEXT_PUBLIC_SUPABASE_URL

  • Look for "Project URL"
  • Copy the URL (looks like https://xxxxx.supabase.co)
NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co

NEXT_PUBLIC_SUPABASE_ANON_KEY

  • Look for "Project API keys"
  • Copy the anon public key (the shorter one, safe for browsers)
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

SUPABASE_SERVICE_ROLE_KEY

  • On the same page, find the service_role key
  • ⚠️ Click "Reveal" to see it
  • ⚠️ NEVER expose this in client-side code or commit to git!
SUPABASE_SERVICE_ROLE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Step 3: Create Your .env.local File

Create a file named .env.local in your project root with all three variables:

# Supabase Configuration
# Get these from: Supabase Dashboard → Project Settings → API

NEXT_PUBLIC_SUPABASE_URL=https://your-project-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=eyJhbGci...your-anon-key
SUPABASE_SERVICE_ROLE_KEY=eyJhbGci...your-service-role-key

Note: The .env.local file is already in .gitignore and won't be committed.


Database Schema

Run this SQL in your Supabase Dashboard:

  1. Go to SQL Editor (left sidebar)
  2. Click "New query"
  3. Paste the following SQL and click "Run"
-- =============================================
-- FairTime Database Schema
-- =============================================

-- Enable UUID extension (usually already enabled)
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";

-- =============================================
-- Tasks Table
-- =============================================
CREATE TABLE IF NOT EXISTS tasks (
  id TEXT PRIMARY KEY,
  title TEXT NOT NULL,
  description TEXT NOT NULL,
  time_limit_seconds INTEGER NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- =============================================
-- User Sessions Table
-- =============================================
CREATE TABLE IF NOT EXISTS user_sessions (
  id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
  email TEXT NOT NULL,
  task_id TEXT NOT NULL REFERENCES tasks(id),
  started_at TIMESTAMP WITH TIME ZONE,
  deadline_at TIMESTAMP WITH TIME ZONE,
  submitted_at TIMESTAMP WITH TIME ZONE,
  proof_url TEXT,
  status TEXT NOT NULL DEFAULT 'pending',
  created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),

  -- Ensure one session per email per task
  CONSTRAINT unique_email_task UNIQUE(email, task_id),

  -- Validate status values
  CONSTRAINT valid_status CHECK (status IN ('pending', 'in_progress', 'on_time', 'late', 'not_submitted'))
);

-- =============================================
-- Auto-update updated_at Trigger
-- =============================================
CREATE OR REPLACE FUNCTION update_updated_at_column()
RETURNS TRIGGER AS $$
BEGIN
  NEW.updated_at = NOW();
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER update_user_sessions_updated_at
  BEFORE UPDATE ON user_sessions
  FOR EACH ROW
  EXECUTE FUNCTION update_updated_at_column();

-- =============================================
-- Indexes for Performance
-- =============================================
CREATE INDEX IF NOT EXISTS idx_user_sessions_email ON user_sessions(email);
CREATE INDEX IF NOT EXISTS idx_user_sessions_task_id ON user_sessions(task_id);
CREATE INDEX IF NOT EXISTS idx_user_sessions_status ON user_sessions(status);

-- =============================================
-- Seed MVP Task (30 minutes = 1800 seconds)
-- =============================================
INSERT INTO tasks (id, title, description, time_limit_seconds)
VALUES (
  'mvp-task-1',
  'FairTime MVP Task',
  'Complete the assigned task within the time limit. Your timer started when you entered your email. Submit a link to your completed work before the deadline.',
  1800
)
ON CONFLICT (id) DO NOTHING;

Project Structure

fairtask/
├── app/
│   ├── page.tsx                    # Landing page with email form
│   ├── layout.tsx                  # Root layout with fonts
│   ├── globals.css                 # Global styles and CSS variables
│   ├── admin/
│   │   └── page.tsx                # Admin dashboard
│   ├── task/
│   │   └── [sessionId]/
│   │       ├── page.tsx            # Task display (server component)
│   │       ├── TaskClient.tsx      # Task UI (client component)
│   │       ├── loading.tsx         # Loading skeleton
│   │       ├── error.tsx           # Error boundary
│   │       └── not-found.tsx       # 404 page
│   └── api/
│       ├── health/route.ts         # Health check endpoint
│       ├── session/
│       │   ├── start/route.ts      # POST: Create/resume session
│       │   └── [id]/
│       │       ├── route.ts        # GET: Session state
│       │       └── submit/route.ts # POST: Submit proof
│       └── admin/
│           └── submissions/route.ts # GET: All submissions
├── components/
│   ├── Timer.tsx                   # Reusable timer with polling
│   ├── ProofSubmission.tsx         # Submission form + confirmation
│   ├── LoadingSkeleton.tsx         # Loading states
│   └── ErrorBoundary.tsx           # Error handling
├── lib/
│   ├── supabase/
│   │   ├── server.ts               # Server-side Supabase client
│   │   └── client.ts               # Browser-side Supabase client
│   ├── types.ts                    # TypeScript interfaces
│   ├── time.ts                     # Time calculation utilities
│   └── retry.ts                    # Retry with exponential backoff
└── .env.local                      # Environment variables (not in git)

API Endpoints

Method Endpoint Description
POST /api/session/start Create or resume a session
GET /api/session/[id] Get session state with timing
POST /api/session/[id]/submit Submit proof URL
GET /api/admin/submissions List all submissions
GET /api/health Health check

Pages

Route Description
/ Landing page with email input
/task/[sessionId] Task page with timer and submission form
/admin Admin dashboard with all submissions

Customizing the Task

To change the task content or time limit, update the seed data in Supabase:

UPDATE tasks
SET 
  title = 'Your New Task Title',
  description = 'Your task description here...',
  time_limit_seconds = 3600  -- 1 hour
WHERE id = 'mvp-task-1';

Or insert a new task:

INSERT INTO tasks (id, title, description, time_limit_seconds)
VALUES (
  'custom-task-1',
  'Custom Task',
  'Description of what participants need to do...',
  2700  -- 45 minutes
);

Deployment

Vercel (Recommended)

  1. Push your code to GitHub
  2. Go to vercel.com and import your repository
  3. Add environment variables in Vercel dashboard:
    • NEXT_PUBLIC_SUPABASE_URL
    • NEXT_PUBLIC_SUPABASE_ANON_KEY
    • SUPABASE_SERVICE_ROLE_KEY
  4. Deploy!

Other Platforms

The app runs anywhere that supports Next.js:

  • Railway
  • Render
  • DigitalOcean App Platform
  • Self-hosted with npm run build && npm start

Security Notes

  • ⏱️ Server-Authoritative: All time calculations happen on the server
  • 🔒 Service Role Key: Never expose in client-side code
  • 📝 Immutable Submissions: Once submitted, submitted_at cannot be changed
  • 🚫 No Auth (MVP): Admin dashboard is unprotected; add authentication for production

License

MIT


Built with ❤️ using Next.js and Supabase

About

A time tracking for tasks

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published