diff --git a/app/(app)/dashboard/Board.tsx b/app/(app)/dashboard/Board.tsx index ca71f44..90fcd0b 100644 --- a/app/(app)/dashboard/Board.tsx +++ b/app/(app)/dashboard/Board.tsx @@ -8,7 +8,7 @@ import { useState, useTransition } from "react"; import { useRouter } from "next/navigation"; import Column from "./column/Column"; -import { COLUMNS, Task, TaskStatus } from "@/types/tasks"; +import { COLUMNS, Task, TaskStatus } from "@/types/task"; import Header from "./header/Header"; import { mutate } from "swr"; diff --git a/app/(app)/dashboard/card/Card.tsx b/app/(app)/dashboard/card/Card.tsx index 300b602..db8a120 100644 --- a/app/(app)/dashboard/card/Card.tsx +++ b/app/(app)/dashboard/card/Card.tsx @@ -6,12 +6,28 @@ "use client"; import { Loader2 } from "lucide-react"; -import { CardProps, TaskStatus } from "@/types/tasks"; +import { Task, TaskStatus } from "@/types/task"; import CardActions from "./CardActions"; import CardAvatars from "./CardAvatars"; import CardDueDate from "./CardDueDate"; import CardPriority from "./CardPriority"; +/** + * Properties for the Card component. + * + * @interface CardProps + * @property {Task} task - The task data object to render. + * @property {boolean} [isUpdating] - Flag indicating whether the card is currently undergoing an asynchronous update operation. + * @property {(taskId: string, newStatus: TaskStatus) => void} [onStatusChange] - Callback triggered when the task status changes. + * @property {(taskId: string) => void} [onDelete] - Callback triggered when the task is deleted. + */ +export interface CardProps { + task: Task; + isUpdating?: boolean; + onStatusChange?: (taskId: string, newStatus: TaskStatus) => void; + onDelete?: (taskId: string) => void; +} + /** * Renders an interactive card container handling drag-and-drop actions, loading states, * and assembling modular sub-components for priorities, due dates, avatars, and actions. diff --git a/app/(app)/dashboard/card/CardActions.tsx b/app/(app)/dashboard/card/CardActions.tsx index 20cfc9c..c09f7cc 100644 --- a/app/(app)/dashboard/card/CardActions.tsx +++ b/app/(app)/dashboard/card/CardActions.tsx @@ -7,16 +7,29 @@ import { useState, useEffect, useRef } from "react"; import { MoreHorizontal, CornerDownRight, Trash2 } from "lucide-react"; -import { COLUMNS, TaskStatus } from "@/types/tasks"; +import { COLUMNS, TaskStatus } from "@/types/task"; + +/** + * Properties for the CardActions component. + * + * @interface CardActionsProps + * @property {TaskStatus} currentStatus - The current status category of the task. + * @property {boolean} isCreator - Flag indicating whether the current user is the creator of the task. + * @property {(newStatus: TaskStatus) => void} onMove - Callback function triggered when a new status column is selected. + * @property {() => void} onDelete - Callback function triggered when the delete action is selected. + */ +interface CardActionsProps { + currentStatus: TaskStatus; + isCreator: boolean; + onMove: (newStatus: TaskStatus) => void; + onDelete: () => void; +} /** * Renders a mobile-only action menu component allowing users to move a task * between different columns or delete it entirely via a dropdown interface. * - * @param {Object} props - The component props. - * @param {TaskStatus} props.currentStatus - The current status category of the task. - * @param {(newStatus: TaskStatus) => void} props.onMove - Callback function triggered when a new status column is selected. - * @param {() => void} props.onDelete - Callback function triggered when the delete action is selected. + * @param {CardActionsProps} props - The component props. * @returns {JSX.Element} The rendered mobile card actions component. */ export default function CardActions({ @@ -24,12 +37,7 @@ export default function CardActions({ isCreator, onMove, onDelete, -}: { - currentStatus: TaskStatus; - isCreator: boolean; - onMove: (newStatus: TaskStatus) => void; - onDelete: () => void; -}) { +}: CardActionsProps) { const [showMobileActions, setShowMobileActions] = useState(false); const dropdownRef = useRef(null); diff --git a/app/(app)/dashboard/card/CardAvatars.tsx b/app/(app)/dashboard/card/CardAvatars.tsx index 71794b1..3f113af 100644 --- a/app/(app)/dashboard/card/CardAvatars.tsx +++ b/app/(app)/dashboard/card/CardAvatars.tsx @@ -6,49 +6,94 @@ "use client"; import { Crown } from "lucide-react"; -import { Task } from "@/types/tasks"; +import { Task } from "@/types/task"; +import { getInitials } from "@/utils/user"; + +/** + * Properties for the UserAvatar component. + * + * @interface UserAvatarProps + * @property {Task["creator"]} user - The user object to render. + * @property {string} title - The role description (e.g., "Creator" or "Assignee"). + * @property {boolean} [isCreator] - Optional flag indicating if the user is the creator. + */ +interface UserAvatarProps { + user: Task["creator"]; + title: string; + isCreator?: boolean; +} + +/** + * Renders a single user avatar circle with optional crown badge. + * + * @param {UserAvatarProps} props - The component props. + * @returns {JSX.Element} The rendered user avatar component. + */ +function UserAvatar({ user, title, isCreator }: UserAvatarProps) { + const fullName = `${user.firstName} ${user.lastName}`; + const initials = getInitials(user.firstName, user.lastName); + const bgColor = user.color; + + return ( +
+
+ {initials} +
+ + {isCreator && ( + + + + )} +
+ ); +} + +/** + * Properties for the CardAvatars component. + * + * @interface CardAvatarsProps + * @property {Task["creator"]} creator - The creator user object. + * @property {Task["assignees"]} assignees - The list of assigned user objects. + */ +interface CardAvatarsProps { + creator: Task["creator"]; + assignees: Task["assignees"]; +} /** * Renders overlapping avatar indicators for task assignees and a dedicated, crowned badge for the task creator. - * Filters out duplicate entries where the assignee matches the creator. * - * @param {Object} props - The component props. - * @param {Task["creator"]} props.creator - The email or identifier of the task creator. - * @param {Task["assignees"]} [props.assignees=[]] - An array of emails or identifiers for users assigned to the task. - * @returns {JSX.Element | null} The rendered avatars container, or null if neither creator nor assignees exist. + * @param {CardAvatarsProps} props - The component props. + * @returns {JSX.Element | null} The rendered card avatars container or null if no users are available. */ export default function CardAvatars({ creator, assignees = [], -}: { - creator: Task["creator"]; - assignees: Task["assignees"]; -}) { - const filteredAssignees = (assignees || []).filter((a) => a !== creator); +}: CardAvatarsProps) { + const filteredAssignees = (assignees || []).filter( + (assignee) => assignee.id !== creator?.id, + ); if (!creator && filteredAssignees.length === 0) return null; return (
- {filteredAssignees.map((assignee, index) => ( -
- {assignee.substring(0, 2).toUpperCase()} -
+ {filteredAssignees.map((assignee) => ( + ))} + {creator && ( -
- {creator.substring(0, 2).toUpperCase()} - - - -
+ )}
); diff --git a/app/(app)/dashboard/card/CardDueDate.tsx b/app/(app)/dashboard/card/CardDueDate.tsx index 3446dff..d831712 100644 --- a/app/(app)/dashboard/card/CardDueDate.tsx +++ b/app/(app)/dashboard/card/CardDueDate.tsx @@ -4,7 +4,7 @@ */ import { AlertCircle, CalendarDays } from "lucide-react"; -import { Task } from "@/types/tasks"; +import { Task } from "@/types/task"; /** * Renders a due date badge for a card, showing an overdue alert animation diff --git a/app/(app)/dashboard/card/CardPriority.tsx b/app/(app)/dashboard/card/CardPriority.tsx index edfbd77..090a8ba 100644 --- a/app/(app)/dashboard/card/CardPriority.tsx +++ b/app/(app)/dashboard/card/CardPriority.tsx @@ -3,7 +3,7 @@ * @description Client component rendering the dynamic priority badge for a card based on configuration. */ -import { TaskPriority, PRIORITY_CONFIG } from "@/types/tasks"; +import { TaskPriority, PRIORITY_CONFIG } from "@/types/task"; /** * Renders a styled priority badge for a card. diff --git a/app/(app)/dashboard/column/Column.tsx b/app/(app)/dashboard/column/Column.tsx index 3edeec3..97647ba 100644 --- a/app/(app)/dashboard/column/Column.tsx +++ b/app/(app)/dashboard/column/Column.tsx @@ -9,7 +9,31 @@ import { useState } from "react"; import Card from "../card/Card"; import ColumnHeader from "./ColumnHeader"; import ColumnEmptyState from "./ColumnEmptyState"; -import { ColumnProps, TaskStatus } from "@/types/tasks"; +import { Task, TaskStatus } from "@/types/task"; + +/** + * Properties for the Column component. + * + * @interface ColumnProps + * @property {TaskStatus} id - The unique status identifier for the column. + * @property {string} title - The display title of the column. + * @property {string} color - The indicator color styling for the column header. + * @property {number} count - The total count of tasks within this column. + * @property {Task[]} tasks - The array of tasks belonging to this column. + * @property {Set} [updatingTaskIds] - A set of task IDs currently undergoing updates. + * @property {(taskId: string, targetStatus: TaskStatus) => void} [onTaskMove] - Callback triggered when a task is moved to a new status column. + * @property {(taskId: string) => void} [onTaskDelete] - Callback triggered when a task deletion is requested. + */ +export interface ColumnProps { + id: TaskStatus; + title: string; + color: string; + count: number; + tasks: Task[]; + updatingTaskIds?: Set; + onTaskMove?: (taskId: string, targetStatus: TaskStatus) => void; + onTaskDelete?: (taskId: string) => void; +} /** * Renders an interactive board column supporting drag-over drop target indicators, diff --git a/app/(app)/dashboard/column/ColumnHeader.tsx b/app/(app)/dashboard/column/ColumnHeader.tsx index 68cd2d1..f78b133 100644 --- a/app/(app)/dashboard/column/ColumnHeader.tsx +++ b/app/(app)/dashboard/column/ColumnHeader.tsx @@ -7,25 +7,32 @@ import { Plus } from "lucide-react"; +/** + * Properties for the ColumnHeader component. + * + * @interface ColumnHeaderProps + * @property {string} title - The title of the column. + * @property {string} [color] - Tailwind CSS color class for the status indicator dot. + * @property {number} count - The number of tasks currently inside this column. + */ +interface ColumnHeaderProps { + title: string; + color?: string; + count: number; +} + /** * Renders the header section of a column, displaying a color-coded status indicator, * the column name, the total task count badge, and a button to add new tasks. * - * @param {Object} props - The component props. - * @param {string} props.title - The title of the column. - * @param {string} [props.color] - Tailwind CSS color class for the status indicator dot. - * @param {number} props.count - The number of tasks currently inside this column. + * @param {ColumnHeaderProps} props - The component props. * @returns {JSX.Element} The rendered column header component. */ export default function ColumnHeader({ title, color = "bg-primary", count, -}: { - title: string; - color?: string; - count: number; -}) { +}: ColumnHeaderProps) { return (
diff --git a/app/(app)/dashboard/header/Header.tsx b/app/(app)/dashboard/header/Header.tsx index 7a07d5c..5d63d26 100644 --- a/app/(app)/dashboard/header/Header.tsx +++ b/app/(app)/dashboard/header/Header.tsx @@ -11,24 +11,19 @@ import TrashLink from "./TrashLink"; import NewTaskButton from "./NewTaskButton"; import { useState } from "react"; -/** - * Properties for the Header component. - * - * @interface HeaderProps - * @property {(taskId: string) => void} onTaskDelete - Callback function triggered when a task is dropped into the delete zone. - */ -interface HeaderProps { - onTaskDelete: (taskId: string) => void; -} - /** * Renders the dashboard header section featuring title text, a task deletion drop zone, * and conditionally displays the new task action button and trash link based on the drag state. * - * @param {HeaderProps} props - The component props. + * @param {Object} props - The component props. + * @param {(taskId: string) => void} props.onTaskDelete - Callback function triggered when a task is dropped into the delete zone. * @returns {JSX.Element} The rendered dashboard header component. */ -export default function Header({ onTaskDelete }: HeaderProps) { +export default function Header({ + onTaskDelete, +}: { + onTaskDelete: (taskId: string) => void; +}) { const [isDraggingActive, setIsDraggingActive] = useState(false); return ( diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 233ac5d..1eb1ed1 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -6,7 +6,8 @@ import { auth } from "@/auth"; import { redirect } from "next/navigation"; import Board from "./Board"; -import { Task } from "@/types/tasks"; +import { Task } from "@/types/task"; +import { DbUser } from "@/types/user"; import { TaskService } from "@/services/task.service"; /** @@ -28,15 +29,15 @@ export default async function Dashboard() { const assigneesData = await TaskService.findAssigneesForTasks(allTaskIds); - const assigneesMap = new Map(); + const assigneesMap = new Map(); for (const row of assigneesData) { const existing = assigneesMap.get(row.taskId) || []; - assigneesMap.set(row.taskId, [...existing, row.email]); + assigneesMap.set(row.taskId, [...existing, row.user]); } - const tasks: Task[] = rawTasksWithCreator.map(({ task, creatorEmail }) => ({ + const tasks: Task[] = rawTasksWithCreator.map(({ task, user }) => ({ ...task, - creator: creatorEmail, + creator: user, assignees: assigneesMap.get(task.id) || [], isCreator: task.userId === currentUserId, })); diff --git a/app/(app)/trash/TrashList.tsx b/app/(app)/trash/TrashList.tsx index c860390..630f928 100644 --- a/app/(app)/trash/TrashList.tsx +++ b/app/(app)/trash/TrashList.tsx @@ -5,7 +5,7 @@ "use client"; -import { Task, PRIORITY_CONFIG } from "@/types/tasks"; +import { Task, PRIORITY_CONFIG } from "@/types/task"; import { Calendar } from "lucide-react"; import TaskActionButton from "./components/TaskActionButton"; diff --git a/app/(app)/trash/components/TaskActionButton.tsx b/app/(app)/trash/components/TaskActionButton.tsx index ddc2512..fd01949 100644 --- a/app/(app)/trash/components/TaskActionButton.tsx +++ b/app/(app)/trash/components/TaskActionButton.tsx @@ -10,22 +10,29 @@ import { useRouter } from "next/navigation"; import { RotateCcw, Trash2, Loader2 } from "lucide-react"; import { mutate } from "swr"; +/** + * Properties for the TaskActionButton component. + * + * @interface TaskActionButtonProps + * @property {string} taskId - The unique identifier of the target task. + * @property {"restore" | "delete"} action - The type of action to execute (restore or delete). + */ +interface TaskActionButtonProps { + taskId: string; + action: "restore" | "delete"; +} + /** * Renders an action button for either restoring or permanently deleting a task, * managing the request lifecycle and visual transition states. * - * @param {Object} props - The component props. - * @param {string} props.taskId - The unique identifier of the target task. - * @param {"restore" | "delete"} props.action - The type of action to execute (restore or delete). + * @param {TaskActionButtonProps} props - The component props. * @returns {JSX.Element} The rendered task action button component. */ export default function TaskActionButton({ taskId, action, -}: { - taskId: string; - action: "restore" | "delete"; -}) { +}: TaskActionButtonProps) { const router = useRouter(); const [isPending, startTransition] = useTransition(); diff --git a/app/(app)/trash/page.tsx b/app/(app)/trash/page.tsx index a7179f0..e146267 100644 --- a/app/(app)/trash/page.tsx +++ b/app/(app)/trash/page.tsx @@ -3,12 +3,12 @@ * @description Server component rendering the trash management view using the TaskService. */ -import { Task } from "@/db/schema"; import { auth } from "@/auth"; import { redirect } from "next/navigation"; import TrashList from "../trash/TrashList"; import TrashHeader from "../trash/components/TrashHeader"; import { TaskService } from "@/services/task.service"; +import { Task } from "@/types/task"; /** * Renders the trash page verifying user authentication, querying soft-deleted tasks, @@ -26,8 +26,10 @@ export default async function TrashPage() { const rawTrashedTasks = await TaskService.findTrashTasksForUser(currentUserId); - const tasks: Task[] = rawTrashedTasks.map(({ task }) => ({ + const tasks: Task[] = rawTrashedTasks.map(({ task, user }) => ({ ...task, + creator: user, + assignees: [], isCreator: task.userId === currentUserId, })); diff --git a/app/(auth)/register/page.tsx b/app/(auth)/register/page.tsx index 70fa4f6..5aa1583 100644 --- a/app/(auth)/register/page.tsx +++ b/app/(auth)/register/page.tsx @@ -34,15 +34,30 @@ export default function RegisterPage() { setLoading(true); const formData = new FormData(event.currentTarget); + const firstName = formData.get("firstName"); + const lastName = formData.get("lastName"); const email = formData.get("email"); const password = formData.get("password"); + const confirmPassword = formData.get("confirmPassword"); + + if (password !== confirmPassword) { + setError("Passwords do not match"); + setLoading(false); + return; + } try { // Send registration request to the API const response = await fetch("/api/auth/register", { method: "POST", headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ email, password }), + body: JSON.stringify({ + firstName, + lastName, + email, + password, + confirmPassword, + }), }); // Ensure the response is valid JSON before parsing @@ -95,17 +110,40 @@ export default function RegisterPage() { {/* Credentials Form */}
+
+ + +
+ diff --git a/app/api/auth/register/route.ts b/app/api/auth/register/route.ts index 1859e0f..15a4cdb 100644 --- a/app/api/auth/register/route.ts +++ b/app/api/auth/register/route.ts @@ -1,6 +1,6 @@ /** * @file route.ts - * @description API route handler for user registration, managing email normalization, credential validation, secure password hashing, and auto sign-in. + * @description API route handler for user registration, managing name validation, email normalization, password matching, hashing, and auto sign-in. */ import { NextResponse } from "next/server"; @@ -9,19 +9,19 @@ import { usersTable } from "@/db/schema"; import { eq } from "drizzle-orm"; import bcrypt from "bcryptjs"; import { signIn } from "@/auth"; +import { AVAILABLE_COLORS } from "@/types/user"; /** - * Handles POST requests to register a new user. - * Parses and validates the request body, normalizes the email address, enforces security rules, - * checks for existing records, stores the hashed password in the database, and attempts an automatic sign-in. + * Handles POST requests for new user registration. + * Validates input fields, checks password confirmation, normalizes email, checks for existing users, + * assigns a random profile color, hashes the password, saves the user to the database, and performs an automatic sign-in. * * @async - * @param {Request} request - The incoming HTTP request containing the registration data. - * @returns {Promise} A JSON response with status details indicating success or failure. + * @param {Request} request - The incoming HTTP request containing the registration payload in JSON format. + * @returns {Promise} A JSON response indicating registration success or an error message with the appropriate HTTP status code. */ export async function POST(request: Request) { try { - // Parse incoming JSON body safely const body = await request.json().catch(() => null); if (!body) { @@ -31,22 +31,42 @@ export async function POST(request: Request) { ); } - const { email: rawEmail, password } = body; + const { + firstName, + lastName, + email: rawEmail, + password, + confirmPassword, + } = body; - // Ensure types and presence of required fields + // Validation of all required fields if ( + !firstName || + typeof firstName !== "string" || + !lastName || + typeof lastName !== "string" || !rawEmail || typeof rawEmail !== "string" || !password || - typeof password !== "string" + typeof password !== "string" || + !confirmPassword || + typeof confirmPassword !== "string" ) { return NextResponse.json( - { message: "Email and password are required" }, + { message: "All fields are required" }, { status: 400 }, ); } - // Normalize email (lowercase and trim) + // Check if passwords match + if (password !== confirmPassword) { + return NextResponse.json( + { message: "Passwords do not match" }, + { status: 400 }, + ); + } + + // Normalize email const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; const email = rawEmail.toLowerCase().trim(); @@ -57,7 +77,7 @@ export async function POST(request: Request) { ); } - // Enforce minimum password length security constraint + // Password length check if (password.length < 8) { return NextResponse.json( { message: "Password must be at least 8 characters long" }, @@ -65,7 +85,7 @@ export async function POST(request: Request) { ); } - // Check if user with this email already exists + // Check existing user const existing = await db.query.usersTable.findFirst({ where: eq(usersTable.email, email), }); @@ -77,11 +97,21 @@ export async function POST(request: Request) { ); } - // Hash password securely with bcrypt before storing - const hashedPassword = await bcrypt.hash(password, 10); - await db.insert(usersTable).values({ email, password: hashedPassword }); + // Select a random default color from the palette + const randomColor = + AVAILABLE_COLORS[Math.floor(Math.random() * AVAILABLE_COLORS.length)]; - // Automatically authenticate the user after successful registration + // Hash password & save user + const hashedPassword = await bcrypt.hash(password, 10); + await db.insert(usersTable).values({ + firstName: firstName.trim(), + lastName: lastName.trim(), + email, + password: hashedPassword, + color: randomColor, + }); + + // Auto sign-in const signInResult = await signIn("credentials", { email, password, diff --git a/app/api/tasks/[id]/route.ts b/app/api/tasks/[id]/route.ts index c9814af..e0a0345 100644 --- a/app/api/tasks/[id]/route.ts +++ b/app/api/tasks/[id]/route.ts @@ -7,10 +7,16 @@ import { NextResponse } from "next/server"; import { auth } from "@/auth"; import { taskStatusEnum } from "@/db/schema"; import { TaskService } from "@/services/task.service"; -import { RouteContext, TaskStatus } from "@/types/tasks"; +import { RouteContext, TaskStatus } from "@/types/task"; /** - * Handles PATCH requests to either update a task's status or restore a soft-deleted task. + * Handles PATCH requests to either update a task's status or restore a soft-deleted task, + * validating user sessions, request payloads, and authorization permissions. + * + * @async + * @param {Request} request - The incoming HTTP request object. + * @param {RouteContext} context - The route context containing dynamic route parameters. + * @returns {Promise} A JSON response containing the updated/restored task or an error message. */ export async function PATCH(request: Request, context: RouteContext) { try { @@ -85,7 +91,12 @@ export async function PATCH(request: Request, context: RouteContext) { /** * Handles DELETE requests to either move a task to trash (Soft Delete) - * or permanently delete it if it is already in the trash. + * or permanently delete it if it is already in the trash, verifying session authorization. + * + * @async + * @param {Request} request - The incoming HTTP request object containing query parameters. + * @param {RouteContext} context - The route context containing dynamic route parameters. + * @returns {Promise} A JSON response confirming deletion or an error message. */ export async function DELETE(request: Request, context: RouteContext) { try { diff --git a/app/components/layout/UserBadge.jsx b/app/components/layout/UserBadge.jsx deleted file mode 100644 index 80fcfb7..0000000 --- a/app/components/layout/UserBadge.jsx +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @file UserBadge.tsx - * @description Component rendering a user avatar badge with name and online status indicator. - */ - -import Image from "next/image"; - -/** - * Renders a user profile badge including an avatar image, name, and current status. - * - * @returns {JSX.Element} The rendered user badge component. - */ -export default function UserBadge() { - return ( -
-
- User Avatar -
-
-

Charlotte W.

-

Online

-
-
- ); -} diff --git a/app/components/layout/UserBadge.tsx b/app/components/layout/UserBadge.tsx new file mode 100644 index 0000000..98f7625 --- /dev/null +++ b/app/components/layout/UserBadge.tsx @@ -0,0 +1,41 @@ +/** + * @file UserBadge.tsx + * @description Server component rendering the authenticated user profile badge with initials and name. + */ + +import { auth } from "@/auth"; +import { UserService } from "@/services/user.service"; +import { getInitials } from "@/utils/user"; + +/** + * Renders a user profile badge including an avatar initial circle with user color and full name. + * + * @async + * @returns {Promise} The rendered user badge component, or null if unauthenticated. + */ +export default async function UserBadge() { + const session = await auth(); + if (!session?.user?.id) return null; + + const user = await UserService.findProfileById(session.user.id); + if (!user) return null; + + const fullName = `${user.firstName} ${user.lastName}`.trim(); + const initials = getInitials(user.firstName, user.lastName); + const bgColor = user.color || "bg-primary"; + + return ( +
+
+ {initials} +
+
+

{fullName}

+

Online

+
+
+ ); +} diff --git a/db/schema.ts b/db/schema.ts index 68b153f..9862a7d 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -46,6 +46,9 @@ export const usersTable = pgTable("users", { id: uuid("id").defaultRandom().primaryKey(), email: text("email").notNull().unique(), password: text("password").notNull(), + firstName: text("first_name").notNull(), + lastName: text("last_name").notNull(), + color: text("color").default("bg-indigo-500").notNull(), createdAt: timestamp("created_at").defaultNow().notNull(), }); @@ -91,3 +94,4 @@ export const taskAssigneesTable = pgTable( // ========================================== export type Task = typeof tasksTable.$inferSelect; +export type User = typeof usersTable.$inferSelect; diff --git a/services/task.service.ts b/services/task.service.ts index 824c1e9..a6f39be 100644 --- a/services/task.service.ts +++ b/services/task.service.ts @@ -4,13 +4,8 @@ */ import { db } from "@/db"; -import { - tasksTable, - taskAssigneesTable, - type Task as DbTask, - usersTable, -} from "@/db/schema"; -import { TaskStatus } from "@/types/tasks"; +import { tasksTable, taskAssigneesTable, usersTable } from "@/db/schema"; +import { DbTask, TaskStatus } from "@/types/task"; import { and, eq, or, exists, isNotNull, isNull, inArray } from "drizzle-orm"; /** @@ -112,7 +107,7 @@ export class TaskService { return await db .select({ task: tasksTable, - creatorEmail: usersTable.email, + user: usersTable, }) .from(tasksTable) .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) @@ -137,7 +132,7 @@ export class TaskService { return await db .select({ taskId: taskAssigneesTable.taskId, - email: usersTable.email, + user: usersTable, }) .from(taskAssigneesTable) .innerJoin(usersTable, eq(taskAssigneesTable.userId, usersTable.id)) @@ -155,7 +150,7 @@ export class TaskService { return await db .select({ task: tasksTable, - creatorEmail: usersTable.email, + user: usersTable, }) .from(tasksTable) .innerJoin(usersTable, eq(tasksTable.userId, usersTable.id)) diff --git a/services/user.service.ts b/services/user.service.ts new file mode 100644 index 0000000..d25aced --- /dev/null +++ b/services/user.service.ts @@ -0,0 +1,33 @@ +/** + * @file services/user.service.ts + * @description Business logic service handling user-related database queries. + */ + +import { db } from "@/db"; +import { usersTable } from "@/db/schema"; +import { eq } from "drizzle-orm"; + +/** + * Service class for handling user operations and database interactions. + */ +export class UserService { + /** + * Retrieves specific profile information (firstName, lastName, color) for a user by ID. + * + * @async + * @param {string} userId - The unique identifier of the user. + * @returns {Promise<{ firstName: string; lastName: string; color: string } | null>} The user profile data or null. + */ + static async findProfileById(userId: string) { + const [user] = await db + .select({ + firstName: usersTable.firstName, + lastName: usersTable.lastName, + color: usersTable.color, + }) + .from(usersTable) + .where(eq(usersTable.id, userId)); + + return user || null; + } +} diff --git a/types/tasks.ts b/types/task.ts similarity index 63% rename from types/tasks.ts rename to types/task.ts index 6e0adbb..8cec935 100644 --- a/types/tasks.ts +++ b/types/task.ts @@ -1,18 +1,20 @@ /** - * @file types/tasks.ts - * @description Type definitions, interfaces, and UI configuration mappings for task management and views. + * @file types/task.ts + * @description Central type definitions and global configurations for task management. */ import { taskPriorityEnum, taskStatusEnum, type Task as DbTask, + type User as DbUser, } from "@/db/schema"; // ========================================== // Types // ========================================== +export type { DbTask }; export type TaskStatus = (typeof taskStatusEnum.enumValues)[number]; export type TaskPriority = (typeof taskPriorityEnum.enumValues)[number]; @@ -20,14 +22,10 @@ export type TaskPriority = (typeof taskPriorityEnum.enumValues)[number]; // Interfaces // ========================================== -export interface Task extends Omit { - assignees?: string[]; - creator?: string; - isCreator?: boolean; -} - -export interface RouteContext { - params: Promise<{ id: string }>; +export interface Task extends DbTask { + creator: DbUser; + assignees: DbUser[]; + isCreator: boolean; } export interface TaskPriorityConfig { @@ -35,25 +33,8 @@ export interface TaskPriorityConfig { className: string; } -export interface ColumnConfig { - id: TaskStatus; - title: string; - color: string; -} - -export interface ColumnProps extends ColumnConfig { - count: number; - tasks: Task[]; - updatingTaskIds?: Set; - onTaskMove?: (taskId: string, targetStatus: TaskStatus) => void; - onTaskDelete?: (taskId: string) => void; -} - -export interface CardProps { - task: Task; - isUpdating?: boolean; - onStatusChange?: (taskId: string, newStatus: TaskStatus) => void; - onDelete?: (taskId: string) => void; +export interface RouteContext { + params: Promise<{ id: string }>; } // ========================================== @@ -80,4 +61,4 @@ export const COLUMNS = [ { id: "in_progress", title: "In Progress", color: "bg-indigo-500" }, { id: "await_feedback", title: "Await Feedback", color: "bg-amber-500" }, { id: "done", title: "Done", color: "bg-emerald-500" }, -] as const satisfies readonly ColumnConfig[]; +] as const; diff --git a/types/user.ts b/types/user.ts new file mode 100644 index 0000000..ae505d6 --- /dev/null +++ b/types/user.ts @@ -0,0 +1,27 @@ +/** + * @file types/user.ts + * @description Type definitions and constants related to user entities and UI preferences. + */ + +import { type User as DbUser } from "@/db/schema"; + +// ========================================== +// Types +// ========================================== + +export type { DbUser }; +export type UserColor = (typeof AVAILABLE_COLORS)[number]; + +// ========================================== +// UI Configurations +// ========================================== + +export const AVAILABLE_COLORS = [ + "bg-indigo-500", + "bg-emerald-500", + "bg-amber-500", + "bg-rose-500", + "bg-violet-500", + "bg-sky-500", + "bg-teal-500", +] as const satisfies readonly string[]; diff --git a/utils/user.ts b/utils/user.ts new file mode 100644 index 0000000..88e104c --- /dev/null +++ b/utils/user.ts @@ -0,0 +1,17 @@ +/** + * @file utils/user.ts + * @description Utility functions for user formatting and initials generation. + */ + +/** + * Generates uppercase initials from a user's first and last name. + * + * @param {string} [firstName] - The user's first name. + * @param {string} [lastName] - The user's last name. + * @returns {string} The computed initials (e.g., "JD") or a question mark if neither is provided. + */ +export function getInitials(firstName?: string, lastName?: string): string { + const first = firstName?.[0] || ""; + const last = lastName?.[0] || ""; + return `${first}${last}`.toUpperCase() || "?"; +}