Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 59 additions & 38 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,43 +1,64 @@
# Taskly

Taskly is a simple task management application built with Laravel, Inertia.js, and Vue.js. It allows users to create and manage tasks within teams.
Taskly is a modern, collaborative task management application for teams. It allows teams to create, organize, and track tasks dynamically on an interactive Kanban board.

This project was bootstrapped from the official [Laravel Vue Starter Kit](https://github.com/laravel/vue-starter-kit).

## Features

- **Interactive Kanban Board:** Drag-and-drop tasks between columns, rename columns, and adjust task priority sequences.
- **Advanced Task Filters:** Filter bar for finding tasks by:
- Search keywords (debounced title & description search).
- Assignee (filtered by specific team members or unassigned tasks).
- Tags (multi-select filter).
- Due Dates (shortcuts like Today, This Week, Overdue, or custom exact dates and date ranges).
- **Task Details & Collaboration:** Task editing with rich description styling (Tiptap editor), comments with threaded replies, and a detailed activity log timeline.
- **Workload Dashboard:** Team workspace overview featuring task metrics, urgent "To Handle Now" tasks, column breakdowns, and a recent activity feed (with direct links back to tasks).

## Tech Stack

- **Backend:** Laravel 12 (PHP >= 8.2), Inertia.js V3
- **Frontend:** Vue 3
- **Styling:** Tailwind CSS & Shadcn
- **CI/CD:** GitHub Actions workflow for automated testing, linting and deployment.
- **Deployment:** Docker, with a production-ready Dockerfile included in the repository.

## Installation

### Pre-requirements

- PHP >= 8.5
- Composer
- Node.js >= 24
- NPM >= 11.6.2

### Installation steps

1. Clone the project repository
2. Install dependencies and initialize the environment:
```
composer run setup
```
This script :
- installs the PHP and JavaScript dependencies
- configures the .env file
- generates the Laravel application key
3. Start the development server :
```
composer run dev
```
You can access the application at : http://localhost:8000.

## Project Presentation

The application is a **team-based task manager**.\
It contains **3 teams and 6 users**. Each user belongs to a team.\
Test accounts are available :
| User | EEmail | Password |
| ----------- | ------------------- | ------------ |
| User 1 | test1@example.com | password |
| User 2 | test2@example.com | password |
| ... | test{N}@example.com | password |
| User 6 | test6@example.com | password |

You can also create your own account.
### Prerequisites

Make sure you have the following installed on your local system:
- **PHP** >= 8.2
- **Composer**
- **Node.js** >= 20
- **NPM**

### Getting Started

1. **Clone the repository** to your local environment.
2. **Install dependencies and seed the database:**
```bash
composer run setup
```
*This command installs Composer and JavaScript dependencies, initializes your `.env` configuration, generates the app key, and runs migrations with database seeds.*

3. **Start the development server:**
```bash
composer run dev
```
*This starts the Artisan server and the Vite dev server concurrently.*

4. Open [http://localhost:8000](http://localhost:8000) in your browser.

## Seeded Test Accounts

The seeded database contains **3 teams** and **6 users**. Each user is pre-assigned to a team:

| Team | User | Email | Password |
| :--- | :--- | :--- | :--- |
| **Team 1** | User 1, 2, 3 | `test1@example.com` to `test3@example.com` | `password` |
| **Team 2** | User 4, 5 | `test4@example.com` & `test5@example.com` | `password` |
| **Team 3** | User 6 | `test6@example.com` | `password` |

*Note: You can also register a new account and create a custom team from the login page.*

11 changes: 3 additions & 8 deletions resources/js/components/AppSidebar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { dashboard } from '@/routes';
import { index } from '@/routes/tasks';
import { type NavItem } from '@/types';
import { Link } from '@inertiajs/vue3';
import { BookOpen, Folder, LayoutGrid, ListCheck, Tag } from 'lucide-vue-next';
import { Folder, LayoutGrid, ListCheck, Tag } from 'lucide-vue-next';
import AppLogo from './AppLogo.vue';

const mainNavItems: NavItem[] = [
Expand All @@ -40,14 +40,9 @@ const mainNavItems: NavItem[] = [
const footerNavItems: NavItem[] = [
{
title: 'Github Repo',
href: 'https://github.com/laravel/vue-starter-kit',
href: 'https://github.com/mateocarciu/Taskly',
icon: Folder,
},
{
title: 'Documentation',
href: 'https://laravel.com/docs/starter-kits#vue',
icon: BookOpen,
},
}
];
</script>

Expand Down
5 changes: 4 additions & 1 deletion resources/js/components/dashboard/DashboardAttentionCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,17 @@ onBeforeUnmount(() => {
<Card class="w-full min-w-0">
<CardHeader class="pb-2">
<CardTitle class="text-base">To Handle Now</CardTitle>
<p class="text-xs text-muted-foreground">
Tasks that need your immediate attention.
</p>
</CardHeader>
<CardContent class="p-0">
<div
ref="attentionScrollEl"
class="relative max-h-64 space-y-2 overflow-y-auto sm:max-h-80 md:max-h-96"
@scroll="updateAttentionScrollHint"
>
<div class="space-y-2 px-3 pt-3 sm:px-4 sm:pt-4">
<div class="space-y-2 px-3 sm:px-4">
<Link
v-for="task in tasks"
:key="task.id"
Expand Down
33 changes: 19 additions & 14 deletions resources/js/components/tasks/TaskActivityDiscussionPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import InputError from '@/components/InputError.vue';
import TaskCommentThreadItem from '@/components/tasks/TaskCommentThreadItem.vue';
import TaskRichTextEditor from '@/components/tasks/TaskRichTextEditor.vue';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Label } from '@/components/ui/label';
import { Spinner } from '@/components/ui/spinner';
Expand Down Expand Up @@ -104,24 +103,30 @@ const daysInColumnLabel = computed(() => {
<div class="mb-4 space-y-3">
<h3 class="text-base font-semibold">Discussion</h3>
<div
class="flex flex-wrap items-center gap-2 text-xs text-muted-foreground"
class="flex flex-wrap items-center gap-3"
>
<Badge
<div
v-if="dueDateLabel"
:variant="isOverdue ? 'destructive' : 'secondary'"
class="gap-1"
:class="[
'flex items-center gap-1.5 transition-colors',
isOverdue
? 'bg-destructive/10 text-destructive border border-destructive/20 px-2 py-0.5 rounded-md font-semibold text-[11px]'
: 'text-xs text-muted-foreground font-medium'
]"
title="Due date"
>
<Calendar class="size-3" />
{{ dueDateLabel }}
</Badge>
<Badge
<Calendar class="size-3.5" />
<span>{{ dueDateLabel }}</span>
</div>

<div
v-if="daysInColumnLabel"
variant="secondary"
class="gap-1"
class="flex items-center gap-1.5 text-xs text-muted-foreground font-medium"
title="Time in column"
>
<ClockAlert class="size-3" />
{{ daysInColumnLabel }}
</Badge>
<ClockAlert class="size-3.5 text-muted-foreground" />
<span>{{ daysInColumnLabel }}</span>
</div>
</div>
</div>

Expand Down
20 changes: 12 additions & 8 deletions resources/js/components/tasks/TaskEditFormPanel.vue
Original file line number Diff line number Diff line change
Expand Up @@ -43,11 +43,12 @@ const onTagsUpdated = (tags: Tag[]) => {
</script>

<template>
<div class="h-full min-h-0 p-6 lg:overflow-y-auto lg:overscroll-contain lg:pr-8">
<form
class="space-y-6"
@submit.prevent="$emit('submit')"
>
<form
class="space-y-6 p-6 lg:space-y-0 lg:p-0 lg:flex lg:flex-col lg:h-full lg:min-h-0"
@submit.prevent="$emit('submit')"
>
<!-- Scrollable Content on desktop, normal flow on mobile -->
<div class="lg:flex-1 lg:overflow-y-auto lg:overscroll-contain lg:p-6 lg:pr-8">
<div class="space-y-4">
<div class="grid gap-2">
<Label for="edit-task-title">Title</Label>
Expand Down Expand Up @@ -109,8 +110,11 @@ const onTagsUpdated = (tags: Tag[]) => {
@update:selected="onTagsUpdated"
/>
</div>
</div>

<DialogFooter class="pt-4">
<!-- Sticky Footer on desktop, normal flow on mobile -->
<div class="pt-4 lg:pt-0 lg:shrink-0 lg:border-t lg:border-border lg:bg-background lg:px-6 lg:py-4">
<DialogFooter>
<Button
type="button"
variant="outline"
Expand All @@ -129,6 +133,6 @@ const onTagsUpdated = (tags: Tag[]) => {
Save changes
</Button>
</DialogFooter>
</form>
</div>
</div>
</form>
</template>
36 changes: 18 additions & 18 deletions resources/js/components/tasks/TaskItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
import TagBadge from '@/components/tags/TagBadge.vue';
import TaskDeleteDialog from '@/components/tasks/TaskDeleteDialog.vue';
import { Avatar, AvatarFallback } from '@/components/ui/avatar';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { useInitials } from '@/composables/useInitials';
Expand Down Expand Up @@ -45,7 +44,7 @@ const deleteTask = () => {
@click.stop="emit('edit', task)"
class="group relative cursor-pointer border-border transition-all hover:border-primary/40 hover:bg-accent/5 hover:shadow-sm"
>
<CardContent class="flex flex-col gap-3 p-3.5">
<CardContent class="flex flex-col gap-3 pt-3.5">
<!-- Header: Title and Actions -->
<div class="flex items-start justify-between gap-2">
<p
Expand Down Expand Up @@ -95,43 +94,44 @@ const deleteTask = () => {
</div>

<!-- Footer: Meta tags & Avatar -->
<div class="mt-1 flex items-end justify-between gap-2">
<!-- Tags -->
<div class="flex flex-wrap items-center gap-2">
<Badge
<div
class="mt-2 pt-2.5 border-t border-border/40 flex items-center justify-between gap-2"
>
<div class="flex flex-wrap items-center gap-3">
<div
v-if="
task.days_in_column !== null &&
task.days_in_column !== undefined
"
variant="secondary"
class="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium"
class="flex items-center gap-1.5 text-[11px] text-muted-foreground/80 font-medium"
title="Time in column"
>
<ClockAlert class="size-3 text-muted-foreground" />
<ClockAlert class="size-3.5 text-muted-foreground/80" />
<span>{{
task.days_in_column === 0
? 'Today'
: `${task.days_in_column}d`
}}</span>
</Badge>
</div>

<Badge
<div
v-if="task.due_date"
:variant="isOverdue(task) ? 'destructive' : 'secondary'"
class="flex items-center gap-1 px-1.5 py-0.5 text-[10px] font-medium"
:class="[
'flex items-center gap-1.5 transition-colors',
isOverdue(task)
? 'bg-destructive/10 text-destructive border border-destructive/20 px-2 py-0.5 rounded-md font-semibold text-[10px]'
: 'text-[11px] text-muted-foreground/80 font-medium'
]"
title="Due date"
>
<Calendar
class="size-3"
:class="!isOverdue(task) && 'text-muted-foreground'"
/>
<Calendar class="size-3.5" />
<span>{{
new Date(task.due_date).toLocaleDateString(
'en-US',
{ day: 'numeric', month: 'short' },
)
}}</span>
</Badge>
</div>
</div>

<!-- Avatar -->
Expand Down
Loading