Skip to content
Open
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
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
DATABASE_URL="file:./dev.db"
3 changes: 3 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": "next/core-web-vitals"
}
40 changes: 40 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# local env files
.env*.local

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# prisma
prisma/dev.db
prisma/dev.db-journal
15 changes: 15 additions & 0 deletions app/api/stats/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { NextResponse } from "next/server";
import { getTaskStats } from "@/app/services/taskService";

export async function GET() {
try {
const stats = await getTaskStats();
return NextResponse.json(stats);
} catch (error) {
console.error("Error fetching stats:", error);
return NextResponse.json(
{ error: "Failed to fetch stats" },
{ status: 500 }
);
}
}
82 changes: 82 additions & 0 deletions app/api/tasks/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from "next/server";
import { getTaskById, updateTask, deleteTask } from "@/app/services/taskService";

export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const task = await getTaskById(params.id);

if (!task) {
return NextResponse.json(
{ error: "Task not found" },
{ status: 404 }
);
}

return NextResponse.json(task);
} catch (error) {
console.error("Error fetching task:", error);
return NextResponse.json(
{ error: "Failed to fetch task" },
{ status: 500 }
);
}
}

export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const body = await request.json();

const existingTask = await getTaskById(params.id);
if (!existingTask) {
return NextResponse.json(
{ error: "Task not found" },
{ status: 404 }
);
}

const task = await updateTask(params.id, {
title: body.title,
description: body.description,
status: body.status,
priority: body.priority,
});

return NextResponse.json(task);
} catch (error) {
console.error("Error updating task:", error);
return NextResponse.json(
{ error: "Failed to update task" },
{ status: 500 }
);
}
}

export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
try {
const existingTask = await getTaskById(params.id);
if (!existingTask) {
return NextResponse.json(
{ error: "Task not found" },
{ status: 404 }
);
}

await deleteTask(params.id);
return NextResponse.json({ success: true });
} catch (error) {
console.error("Error deleting task:", error);
return NextResponse.json(
{ error: "Failed to delete task" },
{ status: 500 }
);
}
}
53 changes: 53 additions & 0 deletions app/api/tasks/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { getTasks, createTask } from "@/app/services/taskService";
import { TaskFilters } from "@/app/types";

export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url);

const filters: TaskFilters = {
status: (searchParams.get("status") as TaskFilters["status"]) || "ALL",
priority: (searchParams.get("priority") as TaskFilters["priority"]) || "ALL",
sortBy: (searchParams.get("sortBy") as TaskFilters["sortBy"]) || "createdAt",
sortOrder: (searchParams.get("sortOrder") as TaskFilters["sortOrder"]) || "desc",
};

const tasks = await getTasks(filters);
return NextResponse.json(tasks);
} catch (error) {
console.error("Error fetching tasks:", error);
return NextResponse.json(
{ error: "Failed to fetch tasks" },
{ status: 500 }
);
}
}

export async function POST(request: NextRequest) {
try {
const body = await request.json();

if (!body.title || body.title.trim() === "") {
return NextResponse.json(
{ error: "Title is required" },
{ status: 400 }
);
}

const task = await createTask({
title: body.title,
description: body.description,
status: body.status,
priority: body.priority,
});

return NextResponse.json(task, { status: 201 });
} catch (error) {
console.error("Error creating task:", error);
return NextResponse.json(
{ error: "Failed to create task" },
{ status: 500 }
);
}
}
33 changes: 33 additions & 0 deletions app/components/EmptyState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
"use client";

import { ClipboardList, Plus } from "lucide-react";

interface EmptyStateProps {
onCreateClick: () => void;
hasFilters?: boolean;
}

export default function EmptyState({ onCreateClick, hasFilters }: EmptyStateProps) {
return (
<div className="flex flex-col items-center justify-center py-16 px-4">
<div className="w-20 h-20 bg-gray-100 rounded-full flex items-center justify-center mb-4">
<ClipboardList className="w-10 h-10 text-gray-400" />
</div>
<h3 className="text-lg font-semibold text-gray-900 mb-2">
{hasFilters ? "没有找到匹配的任务" : "还没有任务"}
</h3>
<p className="text-gray-500 text-center max-w-sm mb-6">
{hasFilters
? "尝试调整筛选条件,或者创建一个新任务"
: "开始管理你的任务吧,点击下面的按钮创建第一个任务"}
</p>
<button
onClick={onCreateClick}
className="inline-flex items-center gap-2 px-5 py-2.5 bg-primary-600 hover:bg-primary-700 text-white font-medium rounded-lg transition-colors"
>
<Plus className="w-5 h-5" />
创建任务
</button>
</div>
);
}
29 changes: 29 additions & 0 deletions app/components/LoadingState.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
"use client";

export default function LoadingState() {
return (
<div className="space-y-4">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-white rounded-xl shadow-card p-5 animate-pulse"
>
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="h-5 w-1/3 bg-gray-200 rounded mb-3"></div>
<div className="h-4 w-2/3 bg-gray-200 rounded mb-4"></div>
<div className="flex gap-2">
<div className="h-6 w-20 bg-gray-200 rounded-full"></div>
<div className="h-6 w-24 bg-gray-200 rounded-full"></div>
</div>
</div>
<div className="flex gap-1">
<div className="h-8 w-8 bg-gray-200 rounded-lg"></div>
<div className="h-8 w-8 bg-gray-200 rounded-lg"></div>
</div>
</div>
</div>
))}
</div>
);
}
111 changes: 111 additions & 0 deletions app/components/StatsPanel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"use client";

import { TaskStats } from "@/app/types";
import {
CheckCircle2,
ListTodo,
AlertCircle,
Clock,
TrendingUp,
Calendar,
} from "lucide-react";

interface StatsPanelProps {
stats: TaskStats;
isLoading: boolean;
}

export default function StatsPanel({ stats, isLoading }: StatsPanelProps) {
if (isLoading) {
return (
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 mb-6">
{[...Array(4)].map((_, i) => (
<div
key={i}
className="bg-white rounded-xl shadow-card p-4 animate-pulse"
>
<div className="h-8 w-16 bg-gray-200 rounded mb-2"></div>
<div className="h-4 w-20 bg-gray-200 rounded"></div>
</div>
))}
</div>
);
}

const statItems = [
{
label: "总任务",
value: stats.total,
icon: <ListTodo className="w-5 h-5" />,
color: "bg-blue-100 text-blue-600",
},
{
label: "已完成",
value: stats.completed,
icon: <CheckCircle2 className="w-5 h-5" />,
color: "bg-green-100 text-green-600",
},
{
label: "完成率",
value: `${stats.completionRate}%`,
icon: <TrendingUp className="w-5 h-5" />,
color: "bg-purple-100 text-purple-600",
},
{
label: "今日新增",
value: stats.todayTasks,
icon: <Calendar className="w-5 h-5" />,
color: "bg-orange-100 text-orange-600",
},
];

return (
<div className="space-y-4 mb-6">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{statItems.map((item, index) => (
<div
key={index}
className="bg-white rounded-xl shadow-card p-4 hover:shadow-soft transition-shadow"
>
<div className="flex items-center gap-3">
<div className={`p-2.5 rounded-lg ${item.color}`}>
{item.icon}
</div>
<div>
<p className="text-2xl font-bold text-gray-900">{item.value}</p>
<p className="text-sm text-gray-500">{item.label}</p>
</div>
</div>
</div>
))}
</div>

<div className="bg-white rounded-xl shadow-card p-4">
<h3 className="text-sm font-medium text-gray-700 mb-3 flex items-center gap-2">
<AlertCircle className="w-4 h-4" />
优先级分布
</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-3 bg-red-50 rounded-lg">
<p className="text-2xl font-bold text-red-600">
{stats.byPriority.HIGH}
</p>
<p className="text-xs text-red-600/70 mt-1">高优先级</p>
</div>
<div className="text-center p-3 bg-yellow-50 rounded-lg">
<p className="text-2xl font-bold text-yellow-600">
{stats.byPriority.MEDIUM}
</p>
<p className="text-xs text-yellow-600/70 mt-1">中优先级</p>
</div>
<div className="text-center p-3 bg-green-50 rounded-lg">
<p className="text-2xl font-bold text-green-600">
{stats.byPriority.LOW}
</p>
<p className="text-xs text-green-600/70 mt-1">低优先级</p>
</div>
</div>
</div>
</div>
);
}
Loading