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.

Tags in Timify provide a flexible way to categorise time entries across projects. Each tag is owned by a user (scoped by userId) and can be attached to any number of time entries through the time_entry_tags join table. The six Server Actions documented here cover the full lifecycle of tags: creating and managing tag records, fetching the current user’s tag library, and toggling tag associations on individual time entries.

createTag

import { createTag } from "@/actions/tags/create-tag";
Inserts a new tag record into the tags table, scoped to the authenticated user. The tag is created with the provided name and an optional hex colour. After a successful insert the action revalidates the /tags path and the get-tags cache tag.

Signature

export async function createTag(
  _prevState: CreateTagState,
  formData: FormData
): Promise<CreateTagState>

Parameters

name
string
required
Display name for the tag. Must be non-empty and at most 16 characters long.
color
string
Optional hex colour string (e.g. "#f59e0b"). Stored as-is; defaults to #9ca3af at the database level when omitted.

Return Type

success
boolean
true when the tag was created.
message
string
Human-readable status message.
tag
Tag
The newly created tag row returned by Drizzle’s .returning() clause. Present only when success is true.

Side Effects

OperationValue
revalidatePath/tags
revalidateTagget-tags

Usage Example

"use client";

import { useActionState } from "react";
import { createTag } from "@/actions/tags/create-tag";

export function CreateTagForm() {
  const [state, formAction, isPending] = useActionState(createTag, null);

  return (
    <form action={formAction}>
      <input name="name" placeholder="Tag name (max 16 chars)" maxLength={16} required />
      <input name="color" type="color" defaultValue="#9ca3af" />

      <button type="submit" disabled={isPending}>
        {isPending ? "Creating…" : "Create Tag"}
      </button>

      {state?.message && (
        <p>{state.message}</p>
      )}
      {state?.success && state.tag && (
        <p>Created tag: {state.tag.name} (ID: {state.tag.id})</p>
      )}
    </form>
  );
}

editTag

import { editTag } from "@/actions/tags/edit-tag";
Updates the name and/or colour of an existing tag identified by id. Authentication is verified before any database write. Cache is invalidated for both the /tags/ path and the get-tags tag upon success.

Signature

export async function editTag(
  _prevState: EditTagState,
  formData: FormData
): Promise<EditTagState>

Parameters

id
string
required
UUID of the tag to update.
name
string
required
New display name. Must be non-empty and at most 16 characters.
color
string
Updated hex colour string. Optional.

Return Type

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

Side Effects

OperationValue
revalidatePath/tags/
revalidateTagget-tags

Usage Example

"use client";

import { useActionState } from "react";
import { editTag } from "@/actions/tags/edit-tag";

export function EditTagForm({ tag }: { tag: { id: string; name: string; color: string } }) {
  const [state, formAction, isPending] = useActionState(editTag, null);

  return (
    <form action={formAction}>
      <input type="hidden" name="id" value={tag.id} />
      <input name="name" defaultValue={tag.name} maxLength={16} required />
      <input name="color" type="color" defaultValue={tag.color} />

      <button type="submit" disabled={isPending}>
        {isPending ? "Saving…" : "Save Changes"}
      </button>

      {state?.message && <p>{state.message}</p>}
    </form>
  );
}

deleteTag

import { deleteTag } from "@/actions/tags/delete-tag";
Permanently removes a tag row. Because time_entry_tags.tag_id is defined with onDelete: "cascade", deleting a tag also removes all its associations with time entries automatically — no orphaned join rows are left behind. The function accepts a plain string argument rather than FormData.

Signature

export async function deleteTag(id: string): Promise<DeleteTagState>

Parameters

id
string
required
UUID of the tag to delete. Returns an error state if the string is empty.

Return Type

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

Side Effects

OperationValue
revalidatePath/tags/
revalidateTagget-tags
Deleting a tag cascades to all rows in time_entry_tags that reference it. Any time entries that had this tag will no longer show it, but the time entries themselves are not deleted.

Usage Example

"use client";

import { deleteTag } from "@/actions/tags/delete-tag";
import { useTransition } from "react";

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

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

getTags

import { getTags } from "@/actions/tags/get-tags";
Returns all tag rows from the tags table. Uses Next.js "use cache" with the get-tags cache tag so repeated calls within a render are served from cache until a mutation revalidates the tag.

Signature

export async function getTags(): Promise<InferSelectModel<typeof tags>[]>

Parameters

This function accepts no parameters.

Return Type

(array)
Tag[]
Array of all tag rows. Each element matches the Tag shape described under createTag. Returns an empty array when no tags exist.
Like getProjects, getTags does not filter by userId at the SQL level. It is designed to be called from Server Components where route-level authentication is enforced. Avoid calling it from public or unauthenticated routes.

Usage Example

// app/tags/page.tsx (Server Component)
import { getTags } from "@/actions/tags/get-tags";

export default async function TagsPage() {
  const tags = await getTags();

  return (
    <ul>
      {tags.map((tag) => (
        <li key={tag.id}>
          <span
            style={{ display: "inline-block", width: 12, height: 12, background: tag.color ?? "#9ca3af", borderRadius: "50%" }}
          />
          {" "}{tag.name}
        </li>
      ))}
    </ul>
  );
}

addTagToTimeEntry

import { addTagToTimeEntry } from "@/actions/time-entry-tags/add-tag-to-time-entry";
Creates a row in the time_entry_tags join table linking a tag to a time entry. A unique index on (time_entry_id, tag_id) prevents duplicate associations — attempting to add the same tag twice will throw a database-level constraint error. The function takes two plain string arguments and is not a useActionState-compatible action — call it directly from useTransition or a server action wrapper.

Signature

export async function addTagToTimeEntry(
  timeEntryId: string,
  tagId: string
): Promise<AddTagToEntryState>

Parameters

timeEntryId
string
required
UUID of the time entry to tag.
tagId
string
required
UUID of the tag to attach.

Return Type

success
boolean
true when the association was created.
message
string
Human-readable status message.

Side Effects

OperationValue
revalidatePath/project/
addTagToTimeEntry does not call revalidateTag("get-time-entries"). If you display tag counts in a cached Server Component, you may need to manually call revalidateTag("get-time-entries") in a wrapper action after this function returns.

Usage Example

"use client";

import { addTagToTimeEntry } from "@/actions/time-entry-tags/add-tag-to-time-entry";
import { useTransition } from "react";

export function TagBadge({
  tag,
  timeEntryId,
  isAttached,
}: {
  tag: { id: string; name: string };
  timeEntryId: string;
  isAttached: boolean;
}) {
  const [isPending, startTransition] = useTransition();

  function handleAdd() {
    startTransition(async () => {
      const result = await addTagToTimeEntry(timeEntryId, tag.id);
      if (!result?.success) console.error(result?.message);
    });
  }

  return (
    <button onClick={handleAdd} disabled={isPending || isAttached}>
      {isAttached ? `✓ ${tag.name}` : `+ ${tag.name}`}
    </button>
  );
}

removeTagFromTimeEntry

import { removeTagFromTimeEntry } from "@/actions/time-entry-tags/remove-tag-from-time-entry";
Deletes the row from time_entry_tags that links the specified tag to the specified time entry. The delete uses AND (tagId = ? AND timeEntryId = ?) so only the exact association is removed — the tag itself and the time entry both remain intact.

Signature

export async function removeTagFromTimeEntry(
  timeEntryId: string,
  tagId: string
): Promise<RemoveTagFromTimeEntryState>

Parameters

timeEntryId
string
required
UUID of the time entry from which the tag should be removed.
tagId
string
required
UUID of the tag to detach.

Return Type

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

Side Effects

OperationValue
revalidatePath/project/

Usage Example

"use client";

import { removeTagFromTimeEntry } from "@/actions/time-entry-tags/remove-tag-from-time-entry";
import { useTransition } from "react";

export function RemoveTagButton({
  timeEntryId,
  tagId,
  tagName,
}: {
  timeEntryId: string;
  tagId: string;
  tagName: string;
}) {
  const [isPending, startTransition] = useTransition();

  return (
    <button
      disabled={isPending}
      onClick={() =>
        startTransition(async () => {
          const result = await removeTagFromTimeEntry(timeEntryId, tagId);
          if (!result?.success) console.error(result?.message);
        })
      }
    >
      {isPending ? "Removing…" : `✕ ${tagName}`}
    </button>
  );
}
Pair addTagToTimeEntry and removeTagFromTimeEntry in a single toggle component. Check whether the tag ID is present in the entry’s tags array (from getTimeEntries) to determine which action to call, and disable the button while isPending to prevent double-submissions.

Build docs developers (and LLMs) love