Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/Aking16/timify/llms.txt

Use this file to discover all available pages before exploring further.

Time entries are the core data unit in Timify — each entry represents a discrete period of tracked work linked to a project. The Server Actions in this section manage the full lifecycle of a time entry: starting a timer, editing metadata, stopping a running timer, and deleting stale records. Two pure utility functions, calculateDuration and formatDuration, are also documented here because they power the duration display logic throughout the application.

createTimeEntry

import { createTimeEntry } from "@/actions/time-entries/create-time-entry";
Starts a new time entry for a given project. Before inserting, the action finds all currently running entries belonging to the authenticated user, calculates their elapsed duration, and marks them as stopped. This ensures only one timer is active at any time. The new entry is inserted with isRunning: true and a server-side default title and description.

Signature

export async function createTimeEntry(
  _prevState: CreateTimeEntryState,
  formData: FormData
): Promise<CreateTimeEntryState>

Parameters

projectID
string
required
UUID of the project this time entry belongs to. Must match an existing row in the projects table.

Return Type

success
boolean
true when the entry was created and any previously running entries were successfully stopped.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/project/
revalidateTagget-time-entries
The action atomically stops all other running entries before creating the new one. If an existing running entry has no startTime recorded, it is skipped with a console error rather than causing the whole action to fail.

Usage Example

"use client";

import { useActionState } from "react";
import { createTimeEntry } from "@/actions/time-entries/create-time-entry";

export function StartTimerButton({ projectId }: { projectId: string }) {
  const [state, formAction, isPending] = useActionState(createTimeEntry, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="projectID" value={projectId} />
      <button type="submit" disabled={isPending}>
        {isPending ? "Starting…" : "▶ Start Timer"}
      </button>
      {state && !state.success && (
        <p>{state.message}</p>
      )}
    </form>
  );
}

editTimeEntry

import { editTimeEntry } from "@/actions/time-entries/edit-time-entry";
Updates the title, description, and time bounds of an existing entry. The action recalculates duration from the provided startTime and endTime before writing to the database — the stored duration value always reflects (endTime - startTime) in whole seconds. If endTime is omitted, duration is calculated as (0 - startTime) which effectively stores a negative value, so always supply endTime when the entry has stopped.

Signature

export async function editTimeEntry(
  _prevState: EditTimeEntryState,
  formData: FormData
): Promise<EditTimeEntryState>

Parameters

id
string
required
UUID of the time entry to update.
title
string
required
New title for the entry. Must be between 4 and 32 characters.
description
string
required
Updated description. Minimum 4 characters.
startTime
string
required
ISO 8601 date-time string representing when the entry started. Parsed with new Date(value).
endTime
string
Optional ISO 8601 date-time string representing when the entry ended. When provided, duration is computed as Math.floor((endTime - startTime) / 1000).

Return Type

success
boolean
true when the update was applied successfully.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/project/
revalidateTagget-time-entries

Usage Example

"use client";

import { useActionState } from "react";
import { editTimeEntry } from "@/actions/time-entries/edit-time-entry";

export function EditTimeEntryForm({
  entry,
}: {
  entry: { id: string; title: string; description: string; startTime: Date; endTime: Date | null };
}) {
  const [state, formAction, isPending] = useActionState(editTimeEntry, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="id" value={entry.id} />
      <input
        name="title"
        defaultValue={entry.title ?? ""}
        minLength={4}
        maxLength={32}
        required
      />
      <textarea name="description" defaultValue={entry.description ?? ""} minLength={4} required />
      <input
        name="startTime"
        type="datetime-local"
        defaultValue={entry.startTime.toISOString().slice(0, 16)}
        required
      />
      {entry.endTime && (
        <input
          name="endTime"
          type="datetime-local"
          defaultValue={entry.endTime.toISOString().slice(0, 16)}
        />
      )}

      <button type="submit" disabled={isPending}>
        {isPending ? "Saving…" : "Save"}
      </button>
      {state?.message && <p>{state.message}</p>}
    </form>
  );
}
Always pass endTime for entries that are no longer running. Omitting it causes the duration to be stored as a large negative number because the formula uses 0 in place of endTime.getTime().

stopTimeEntry

import { stopTimeEntry } from "@/actions/time-entries/stop-time-entry";
Stops a specific running time entry. The action looks up the entry, confirms isRunning is true, then computes duration = Math.floor((now - startTime) / 1000) and persists isRunning: false, endTime: now, and duration. Returns an error state if the entry is already stopped or if it has no startTime.

Signature

export async function stopTimeEntry(
  _prevState: StopTimeEntryState,
  formData: FormData
): Promise<StopTimeEntryState>

Parameters

id
string
required
UUID of the running time entry to stop.

Return Type

success
boolean
true when the entry was stopped and duration persisted.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/project/
revalidateTagget-time-entries

Usage Example

"use client";

import { useActionState } from "react";
import { stopTimeEntry } from "@/actions/time-entries/stop-time-entry";

export function StopTimerButton({ entryId }: { entryId: string }) {
  const [state, formAction, isPending] = useActionState(stopTimeEntry, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="id" value={entryId} />
      <button type="submit" disabled={isPending}>
        {isPending ? "Stopping…" : "⏹ Stop Timer"}
      </button>
      {state && !state.success && <p>{state.message}</p>}
    </form>
  );
}

deleteTimeEntry

import { deleteTimeEntry } from "@/actions/time-entries/delete-time-entry";
Permanently removes a time entry row. Unlike most other actions, deleteTimeEntry accepts a plain string argument rather than FormData — call it directly from a useTransition handler or a Server Action wrapper. The cascade rules on the time_entry_tags table also remove any associated tag relationships automatically.

Signature

export async function deleteTimeEntry(id: string): Promise<DeleteTimeEntryState>

Parameters

id
string
required
UUID of the time entry to delete. Returns an error state when empty.

Return Type

success
boolean
true when the row was deleted.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/project/
revalidateTagget-time-entries

Usage Example

"use client";

import { deleteTimeEntry } from "@/actions/time-entries/delete-time-entry";
import { useTransition } from "react";

export function DeleteEntryButton({ entryId }: { entryId: string }) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() =>
        startTransition(async () => {
          const result = await deleteTimeEntry(entryId);
          if (!result?.success) console.error(result?.message);
        })
      }
    >
      {isPending ? "Deleting…" : "Delete Entry"}
    </button>
  );
}

getTimeEntries

import { getTimeEntries } from "@/actions/time-entries/get-time-entry";
Fetches all time entries for a given project, with their associated tags eagerly joined. The function uses a left join across time_entry_tags and tags, then reduces the flat result set into an array of entries where each entry’s tags field is a fully hydrated array of tag objects. Results are cached under the get-time-entries tag.

Signature

export async function getTimeEntries(id: string): Promise<TimeEntryWithTags[]>

Parameters

id
string
required
UUID of the project whose entries should be returned.

Return Type

(array)
TimeEntryWithTags[]
Array of time entry rows, each augmented with a tags field. Ordered by createdAt ascending.

Usage Example

// app/projects/[id]/page.tsx (Server Component)
import { getTimeEntries } from "@/actions/time-entries/get-time-entry";
import { formatDuration } from "@/lib/calculate-duration";

export default async function ProjectPage({ params }: { params: { id: string } }) {
  const entries = await getTimeEntries(params.id);

  return (
    <ul>
      {entries.map((entry) => (
        <li key={entry.id}>
          <strong>{entry.title}</strong>
          {entry.duration != null && (
            <span> — {formatDuration(entry.duration)}</span>
          )}
          {entry.isRunning && <span> 🔴 Running</span>}
          <ul>
            {entry.tags.map((tag) => (
              <li key={tag.id} style={{ color: tag.color ?? undefined }}>
                {tag.name}
              </li>
            ))}
          </ul>
        </li>
      ))}
    </ul>
  );
}

Utility Functions

The following functions live in src/lib/calculate-duration.ts. They are not Server Actions — they are pure, synchronous helpers that can be called in both Server Components and Client Components.

calculateDuration

Computes elapsed time between a start timestamp and an optional end timestamp. When endTime is omitted, the current moment (new Date()) is used, making it suitable for displaying live running timers.
import { calculateDuration } from "@/lib/calculate-duration";

Signature

export function calculateDuration({
  startTime,
  endTime,
  showFormatted,
}: {
  startTime: Date | null;
  endTime?: Date | null;
  showFormatted: boolean;
}): string | number

Parameters

startTime
Date | null
required
The moment the timer started. Returns 0 immediately when null.
endTime
Date | null
Optional end timestamp. Defaults to new Date() (now) when omitted or null.
showFormatted
boolean
required
When true, returns a formatted string via formatDuration. When false, returns the raw integer number of seconds.

Return Type

(value)
string | number
If showFormatted is true, returns a human-readable string such as "1:23:45" or "05:30". If false, returns the integer number of elapsed seconds.

Usage Example

import { calculateDuration } from "@/lib/calculate-duration";

const seconds = calculateDuration({
  startTime: new Date("2024-01-15T09:00:00Z"),
  endTime: new Date("2024-01-15T10:30:00Z"),
  showFormatted: false,
}); // → 5400

const label = calculateDuration({
  startTime: new Date("2024-01-15T09:00:00Z"),
  endTime: new Date("2024-01-15T10:30:00Z"),
  showFormatted: true,
}); // → "1:30:00"

// For a live running timer — endTime defaults to now
const live = calculateDuration({
  startTime: entry.startTime,
  showFormatted: true,
}); // → "0:07:23" (approximately)

formatDuration

Converts a raw second count into a display string. Shows H:MM:SS format when the duration is one hour or longer, and MM:SS format for sub-hour durations.
import { formatDuration } from "@/lib/calculate-duration";

Signature

export function formatDuration(seconds: number): string

Parameters

seconds
number
required
Total elapsed time in whole seconds. Negative values produce unexpected output — always pass non-negative integers.

Return Type

(string)
string
Formatted duration string. Examples:
  • 3661"1:01:01" (H:MM:SS)
  • 90"1:30" (MM:SS)
  • 5"0:05" (MM:SS)

Usage Example

import { formatDuration } from "@/lib/calculate-duration";

console.log(formatDuration(3661));  // "1:01:01"
console.log(formatDuration(90));    // "1:30"
console.log(formatDuration(5));     // "0:05"
console.log(formatDuration(0));     // "0:00"
Use calculateDuration when you have Date objects and want a one-step call. Use formatDuration directly when you already have a stored duration integer from the database — for example, when rendering a completed (non-running) time entry.

Build docs developers (and LLMs) love