olly-molly 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +182 -0
- package/app/api/agent/execute/route.ts +157 -0
- package/app/api/agent/status/route.ts +38 -0
- package/app/api/check-api-key/route.ts +12 -0
- package/app/api/conversations/[id]/route.ts +35 -0
- package/app/api/conversations/route.ts +24 -0
- package/app/api/members/[id]/route.ts +37 -0
- package/app/api/members/route.ts +12 -0
- package/app/api/pm/breakdown/route.ts +142 -0
- package/app/api/pm/tickets/route.ts +147 -0
- package/app/api/projects/[id]/route.ts +56 -0
- package/app/api/projects/active/route.ts +15 -0
- package/app/api/projects/route.ts +53 -0
- package/app/api/tickets/[id]/logs/route.ts +16 -0
- package/app/api/tickets/[id]/route.ts +60 -0
- package/app/api/tickets/[id]/work-logs/route.ts +16 -0
- package/app/api/tickets/route.ts +37 -0
- package/app/design-system/page.tsx +242 -0
- package/app/favicon.ico +0 -0
- package/app/globals.css +318 -0
- package/app/layout.tsx +37 -0
- package/app/page.tsx +331 -0
- package/bin/cli.js +66 -0
- package/components/ThemeProvider.tsx +56 -0
- package/components/ThemeToggle.tsx +31 -0
- package/components/activity/ActivityLog.tsx +96 -0
- package/components/activity/index.ts +1 -0
- package/components/kanban/ConversationList.tsx +75 -0
- package/components/kanban/ConversationView.tsx +132 -0
- package/components/kanban/KanbanBoard.tsx +179 -0
- package/components/kanban/KanbanColumn.tsx +80 -0
- package/components/kanban/SortableTicket.tsx +58 -0
- package/components/kanban/TicketCard.tsx +98 -0
- package/components/kanban/TicketModal.tsx +510 -0
- package/components/kanban/TicketSidebar.tsx +448 -0
- package/components/kanban/index.ts +8 -0
- package/components/pm/PMRequestModal.tsx +196 -0
- package/components/pm/index.ts +1 -0
- package/components/project/ProjectSelector.tsx +211 -0
- package/components/project/index.ts +1 -0
- package/components/team/MemberCard.tsx +147 -0
- package/components/team/TeamPanel.tsx +57 -0
- package/components/team/index.ts +2 -0
- package/components/ui/ApiKeyModal.tsx +101 -0
- package/components/ui/Avatar.tsx +95 -0
- package/components/ui/Badge.tsx +59 -0
- package/components/ui/Button.tsx +60 -0
- package/components/ui/Card.tsx +64 -0
- package/components/ui/Input.tsx +41 -0
- package/components/ui/Modal.tsx +76 -0
- package/components/ui/ResizablePane.tsx +97 -0
- package/components/ui/Select.tsx +45 -0
- package/components/ui/Textarea.tsx +41 -0
- package/components/ui/index.ts +8 -0
- package/db/dev.sqlite +0 -0
- package/db/dev.sqlite-shm +0 -0
- package/db/dev.sqlite-wal +0 -0
- package/db/schema-conversations.sql +26 -0
- package/db/schema-projects.sql +29 -0
- package/db/schema.sql +94 -0
- package/lib/agent-jobs.ts +232 -0
- package/lib/db.ts +564 -0
- package/next.config.ts +10 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/app-icon.png +0 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/profiles/designer.png +0 -0
- package/public/profiles/dev-backend.png +0 -0
- package/public/profiles/dev-frontend.png +0 -0
- package/public/profiles/pm.png +0 -0
- package/public/profiles/qa.png +0 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/tsconfig.json +34 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ticketService, memberService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* PM Agent API - Create tickets with automatic assignment
|
|
6
|
+
*
|
|
7
|
+
* The PM Agent analyzes the ticket content and automatically assigns
|
|
8
|
+
* it to the most appropriate team member based on keywords and task type.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Keywords mapping for auto-assignment
|
|
12
|
+
const ROLE_KEYWORDS: Record<string, string[]> = {
|
|
13
|
+
FE_DEV: [
|
|
14
|
+
'frontend', 'ui', 'ux', 'react', 'component', 'css', 'style', 'design',
|
|
15
|
+
'button', 'form', 'page', 'layout', 'responsive', 'animation', 'tailwind',
|
|
16
|
+
'프론트엔드', 'UI', '컴포넌트', '디자인', '스타일', '화면', '페이지'
|
|
17
|
+
],
|
|
18
|
+
BACKEND_DEV: [
|
|
19
|
+
'backend', 'api', 'database', 'db', 'server', 'endpoint', 'rest',
|
|
20
|
+
'authentication', 'auth', 'sql', 'query', 'migration', 'model',
|
|
21
|
+
'백엔드', 'API', '데이터베이스', '서버', '인증'
|
|
22
|
+
],
|
|
23
|
+
QA: [
|
|
24
|
+
'test', 'testing', 'qa', 'quality', 'bug', 'fix', 'verify', 'validation',
|
|
25
|
+
'e2e', 'integration', 'unit test', 'playwright', 'automation',
|
|
26
|
+
'테스트', 'QA', '버그', '검증', '품질'
|
|
27
|
+
],
|
|
28
|
+
DEVOPS: [
|
|
29
|
+
'deploy', 'deployment', 'ci', 'cd', 'pipeline', 'docker', 'kubernetes',
|
|
30
|
+
'infrastructure', 'monitoring', 'logging', 'aws', 'cloud', 'server',
|
|
31
|
+
'배포', '인프라', '파이프라인', '모니터링'
|
|
32
|
+
],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
function analyzeAndAssign(title: string, description?: string): string | null {
|
|
36
|
+
const text = `${title} ${description || ''}`.toLowerCase();
|
|
37
|
+
|
|
38
|
+
// Count keyword matches for each role
|
|
39
|
+
const scores: Record<string, number> = {
|
|
40
|
+
FE_DEV: 0,
|
|
41
|
+
BACKEND_DEV: 0,
|
|
42
|
+
QA: 0,
|
|
43
|
+
DEVOPS: 0,
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const [role, keywords] of Object.entries(ROLE_KEYWORDS)) {
|
|
47
|
+
for (const keyword of keywords) {
|
|
48
|
+
if (text.includes(keyword.toLowerCase())) {
|
|
49
|
+
scores[role] += 1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Find the role with highest score
|
|
55
|
+
let maxScore = 0;
|
|
56
|
+
let assignedRole: string | null = null;
|
|
57
|
+
|
|
58
|
+
for (const [role, score] of Object.entries(scores)) {
|
|
59
|
+
if (score > maxScore) {
|
|
60
|
+
maxScore = score;
|
|
61
|
+
assignedRole = role;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// If no clear match, default to FE_DEV for general tasks
|
|
66
|
+
if (maxScore === 0) {
|
|
67
|
+
assignedRole = 'FE_DEV';
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Get the member with this role
|
|
71
|
+
const member = memberService.getByRole(assignedRole!);
|
|
72
|
+
return member?.id || null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function POST(request: NextRequest) {
|
|
76
|
+
try {
|
|
77
|
+
const body = await request.json();
|
|
78
|
+
|
|
79
|
+
if (!body.title) {
|
|
80
|
+
return NextResponse.json(
|
|
81
|
+
{ error: 'Title is required' },
|
|
82
|
+
{ status: 400 }
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Get PM member
|
|
87
|
+
const pmMember = memberService.getByRole('PM');
|
|
88
|
+
|
|
89
|
+
// Auto-assign based on content analysis
|
|
90
|
+
let assigneeId = body.assignee_id;
|
|
91
|
+
let autoAssigned = false;
|
|
92
|
+
|
|
93
|
+
if (!assigneeId && body.auto_assign !== false) {
|
|
94
|
+
assigneeId = analyzeAndAssign(body.title, body.description);
|
|
95
|
+
autoAssigned = true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Create ticket with PM as creator
|
|
99
|
+
const ticket = ticketService.create({
|
|
100
|
+
title: body.title,
|
|
101
|
+
description: body.description,
|
|
102
|
+
priority: body.priority || 'MEDIUM',
|
|
103
|
+
assignee_id: assigneeId,
|
|
104
|
+
created_by: pmMember?.id,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// Get assignee info for response
|
|
108
|
+
const assignee = assigneeId ? memberService.getById(assigneeId) : null;
|
|
109
|
+
|
|
110
|
+
return NextResponse.json({
|
|
111
|
+
...ticket,
|
|
112
|
+
assignee,
|
|
113
|
+
auto_assigned: autoAssigned,
|
|
114
|
+
created_by_pm: true,
|
|
115
|
+
}, { status: 201 });
|
|
116
|
+
} catch (error) {
|
|
117
|
+
console.error('Error in PM create ticket:', error);
|
|
118
|
+
return NextResponse.json(
|
|
119
|
+
{ error: 'Failed to create ticket' },
|
|
120
|
+
{ status: 500 }
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// GET endpoint to get PM agent info and capabilities
|
|
126
|
+
export async function GET() {
|
|
127
|
+
try {
|
|
128
|
+
const pmMember = memberService.getByRole('PM');
|
|
129
|
+
const allMembers = memberService.getAll();
|
|
130
|
+
|
|
131
|
+
return NextResponse.json({
|
|
132
|
+
pm: pmMember,
|
|
133
|
+
team: allMembers.filter(m => m.role !== 'PM'),
|
|
134
|
+
capabilities: {
|
|
135
|
+
auto_assignment: true,
|
|
136
|
+
supported_roles: Object.keys(ROLE_KEYWORDS),
|
|
137
|
+
keywords: ROLE_KEYWORDS,
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error fetching PM info:', error);
|
|
142
|
+
return NextResponse.json(
|
|
143
|
+
{ error: 'Failed to fetch PM info' },
|
|
144
|
+
{ status: 500 }
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { projectService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const project = projectService.getById(id);
|
|
11
|
+
if (!project) {
|
|
12
|
+
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
|
13
|
+
}
|
|
14
|
+
return NextResponse.json(project);
|
|
15
|
+
} catch (error) {
|
|
16
|
+
console.error('Error fetching project:', error);
|
|
17
|
+
return NextResponse.json({ error: 'Failed to fetch project' }, { status: 500 });
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function DELETE(
|
|
22
|
+
request: NextRequest,
|
|
23
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
24
|
+
) {
|
|
25
|
+
try {
|
|
26
|
+
const { id } = await params;
|
|
27
|
+
const deleted = projectService.delete(id);
|
|
28
|
+
if (!deleted) {
|
|
29
|
+
return NextResponse.json({ error: 'Project not found' }, { status: 404 });
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.json({ success: true });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error deleting project:', error);
|
|
34
|
+
return NextResponse.json({ error: 'Failed to delete project' }, { status: 500 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export async function PATCH(
|
|
39
|
+
request: NextRequest,
|
|
40
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
41
|
+
) {
|
|
42
|
+
try {
|
|
43
|
+
const { id } = await params;
|
|
44
|
+
const body = await request.json();
|
|
45
|
+
|
|
46
|
+
if (body.is_active) {
|
|
47
|
+
const project = projectService.setActive(id);
|
|
48
|
+
return NextResponse.json(project);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({ error: 'Invalid update' }, { status: 400 });
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error('Error updating project:', error);
|
|
54
|
+
return NextResponse.json({ error: 'Failed to update project' }, { status: 500 });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { projectService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
const project = projectService.getActive();
|
|
7
|
+
if (!project) {
|
|
8
|
+
return NextResponse.json({ error: 'No active project' }, { status: 404 });
|
|
9
|
+
}
|
|
10
|
+
return NextResponse.json(project);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Error fetching active project:', error);
|
|
13
|
+
return NextResponse.json({ error: 'Failed to fetch active project' }, { status: 500 });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { projectService } from '@/lib/db';
|
|
3
|
+
import fs from 'fs';
|
|
4
|
+
import path from 'path';
|
|
5
|
+
|
|
6
|
+
export async function GET() {
|
|
7
|
+
try {
|
|
8
|
+
const projects = projectService.getAll();
|
|
9
|
+
return NextResponse.json(projects);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error('Error fetching projects:', error);
|
|
12
|
+
return NextResponse.json({ error: 'Failed to fetch projects' }, { status: 500 });
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function POST(request: NextRequest) {
|
|
17
|
+
try {
|
|
18
|
+
const body = await request.json();
|
|
19
|
+
|
|
20
|
+
if (!body.path) {
|
|
21
|
+
return NextResponse.json({ error: 'Project path is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Validate that the path exists and is a directory
|
|
25
|
+
const projectPath = body.path;
|
|
26
|
+
if (!fs.existsSync(projectPath)) {
|
|
27
|
+
return NextResponse.json({ error: 'Path does not exist' }, { status: 400 });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const stats = fs.statSync(projectPath);
|
|
31
|
+
if (!stats.isDirectory()) {
|
|
32
|
+
return NextResponse.json({ error: 'Path is not a directory' }, { status: 400 });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if it's a git repository
|
|
36
|
+
const gitPath = path.join(projectPath, '.git');
|
|
37
|
+
const isGitRepo = fs.existsSync(gitPath);
|
|
38
|
+
|
|
39
|
+
// Extract project name from path if not provided
|
|
40
|
+
const name = body.name || path.basename(projectPath);
|
|
41
|
+
|
|
42
|
+
const project = projectService.create({
|
|
43
|
+
name,
|
|
44
|
+
path: projectPath,
|
|
45
|
+
description: body.description || (isGitRepo ? 'Git repository' : 'Local project'),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return NextResponse.json({ ...project, is_git_repo: isGitRepo }, { status: 201 });
|
|
49
|
+
} catch (error) {
|
|
50
|
+
console.error('Error creating project:', error);
|
|
51
|
+
return NextResponse.json({ error: 'Failed to create project' }, { status: 500 });
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { activityService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const logs = activityService.getByTicketId(id);
|
|
11
|
+
return NextResponse.json(logs);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error fetching activity logs:', error);
|
|
14
|
+
return NextResponse.json({ error: 'Failed to fetch activity logs' }, { status: 500 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ticketService, activityService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const ticket = ticketService.getById(id);
|
|
11
|
+
if (!ticket) {
|
|
12
|
+
return NextResponse.json({ error: 'Ticket not found' }, { status: 404 });
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Include activity logs
|
|
16
|
+
const logs = activityService.getByTicketId(id);
|
|
17
|
+
|
|
18
|
+
return NextResponse.json({ ...ticket, logs });
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error('Error fetching ticket:', error);
|
|
21
|
+
return NextResponse.json({ error: 'Failed to fetch ticket' }, { status: 500 });
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function PATCH(
|
|
26
|
+
request: NextRequest,
|
|
27
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
28
|
+
) {
|
|
29
|
+
try {
|
|
30
|
+
const { id } = await params;
|
|
31
|
+
const body = await request.json();
|
|
32
|
+
const { updated_by, ...data } = body;
|
|
33
|
+
|
|
34
|
+
const ticket = ticketService.update(id, data, updated_by);
|
|
35
|
+
if (!ticket) {
|
|
36
|
+
return NextResponse.json({ error: 'Ticket not found' }, { status: 404 });
|
|
37
|
+
}
|
|
38
|
+
return NextResponse.json(ticket);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('Error updating ticket:', error);
|
|
41
|
+
return NextResponse.json({ error: 'Failed to update ticket' }, { status: 500 });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function DELETE(
|
|
46
|
+
request: NextRequest,
|
|
47
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
48
|
+
) {
|
|
49
|
+
try {
|
|
50
|
+
const { id } = await params;
|
|
51
|
+
const deleted = ticketService.delete(id);
|
|
52
|
+
if (!deleted) {
|
|
53
|
+
return NextResponse.json({ error: 'Ticket not found' }, { status: 404 });
|
|
54
|
+
}
|
|
55
|
+
return NextResponse.json({ success: true });
|
|
56
|
+
} catch (error) {
|
|
57
|
+
console.error('Error deleting ticket:', error);
|
|
58
|
+
return NextResponse.json({ error: 'Failed to delete ticket' }, { status: 500 });
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { agentWorkLogService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(
|
|
5
|
+
request: NextRequest,
|
|
6
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
7
|
+
) {
|
|
8
|
+
try {
|
|
9
|
+
const { id } = await params;
|
|
10
|
+
const logs = agentWorkLogService.getByTicketId(id);
|
|
11
|
+
return NextResponse.json(logs);
|
|
12
|
+
} catch (error) {
|
|
13
|
+
console.error('Error fetching work logs:', error);
|
|
14
|
+
return NextResponse.json({ error: 'Failed to fetch work logs' }, { status: 500 });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { ticketService } from '@/lib/db';
|
|
3
|
+
|
|
4
|
+
export async function GET(request: NextRequest) {
|
|
5
|
+
try {
|
|
6
|
+
const { searchParams } = new URL(request.url);
|
|
7
|
+
const status = searchParams.get('status') || undefined;
|
|
8
|
+
const projectId = searchParams.get('projectId') || undefined;
|
|
9
|
+
const tickets = ticketService.getAll(status, projectId);
|
|
10
|
+
return NextResponse.json(tickets);
|
|
11
|
+
} catch (error) {
|
|
12
|
+
console.error('Error fetching tickets:', error);
|
|
13
|
+
return NextResponse.json({ error: 'Failed to fetch tickets' }, { status: 500 });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export async function POST(request: NextRequest) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await request.json();
|
|
20
|
+
if (!body.title) {
|
|
21
|
+
return NextResponse.json({ error: 'Title is required' }, { status: 400 });
|
|
22
|
+
}
|
|
23
|
+
const ticket = ticketService.create({
|
|
24
|
+
title: body.title,
|
|
25
|
+
description: body.description,
|
|
26
|
+
priority: body.priority,
|
|
27
|
+
assignee_id: body.assignee_id,
|
|
28
|
+
project_id: body.project_id,
|
|
29
|
+
created_by: body.created_by,
|
|
30
|
+
});
|
|
31
|
+
return NextResponse.json(ticket, { status: 201 });
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('Error creating ticket:', error);
|
|
34
|
+
return NextResponse.json({ error: 'Failed to create ticket' }, { status: 500 });
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useState } from 'react';
|
|
4
|
+
import Image from 'next/image';
|
|
5
|
+
import Link from 'next/link';
|
|
6
|
+
import { Button } from '@/components/ui/Button';
|
|
7
|
+
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/Card';
|
|
8
|
+
import { Badge, StatusBadge, PriorityBadge } from '@/components/ui/Badge';
|
|
9
|
+
import { Input } from '@/components/ui/Input';
|
|
10
|
+
import { Textarea } from '@/components/ui/Textarea';
|
|
11
|
+
import { Select } from '@/components/ui/Select';
|
|
12
|
+
import { Modal } from '@/components/ui/Modal';
|
|
13
|
+
import { Avatar } from '@/components/ui/Avatar';
|
|
14
|
+
import { ThemeToggle } from '@/components/ThemeToggle';
|
|
15
|
+
|
|
16
|
+
export default function DesignSystemPage() {
|
|
17
|
+
const [selectValue, setSelectValue] = useState('');
|
|
18
|
+
const [modalOpen, setModalOpen] = useState(false);
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<div className="min-h-screen bg-[var(--bg-primary)]">
|
|
22
|
+
{/* Header */}
|
|
23
|
+
<header className="border-b border-[var(--border-primary)]">
|
|
24
|
+
<div className="max-w-5xl mx-auto px-6 py-4 flex items-center justify-between">
|
|
25
|
+
<div className="flex items-center gap-4">
|
|
26
|
+
<Image
|
|
27
|
+
src="/app-icon.png"
|
|
28
|
+
alt="Olly Molly"
|
|
29
|
+
width={32}
|
|
30
|
+
height={32}
|
|
31
|
+
className="opacity-80"
|
|
32
|
+
/>
|
|
33
|
+
<div>
|
|
34
|
+
<h1 className="text-sm font-medium text-[var(--text-primary)]">Design System</h1>
|
|
35
|
+
<p className="text-xs text-[var(--text-muted)]">Olly Molly</p>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<div className="flex items-center gap-4">
|
|
39
|
+
<Link href="/" className="text-xs text-[var(--text-secondary)] hover:text-[var(--text-primary)]">
|
|
40
|
+
← Back
|
|
41
|
+
</Link>
|
|
42
|
+
<ThemeToggle />
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
</header>
|
|
46
|
+
|
|
47
|
+
<main className="max-w-5xl mx-auto px-6 py-12">
|
|
48
|
+
{/* Intro */}
|
|
49
|
+
<div className="mb-16">
|
|
50
|
+
<h1 className="text-display text-[var(--text-primary)] mb-4">Design System</h1>
|
|
51
|
+
<p className="text-body text-[var(--text-secondary)] max-w-lg">
|
|
52
|
+
Minimal design inspired by fontshare.com. Cream backgrounds, black text, thin borders.
|
|
53
|
+
</p>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
{/* Colors */}
|
|
57
|
+
<section className="mb-16">
|
|
58
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Colors</h2>
|
|
59
|
+
<div className="border-t border-[var(--border-primary)]">
|
|
60
|
+
<ColorRow name="bg-primary" value="#F5F4EE" />
|
|
61
|
+
<ColorRow name="bg-secondary" value="#EFEDE5" />
|
|
62
|
+
<ColorRow name="bg-card" value="#FAFAF7" />
|
|
63
|
+
<ColorRow name="border-primary" value="#E0DED6" />
|
|
64
|
+
<ColorRow name="text-primary" value="#1A1A1A" isText />
|
|
65
|
+
<ColorRow name="text-secondary" value="#4A4A4A" isText />
|
|
66
|
+
<ColorRow name="text-muted" value="#9A9A9A" isText />
|
|
67
|
+
</div>
|
|
68
|
+
</section>
|
|
69
|
+
|
|
70
|
+
{/* Typography */}
|
|
71
|
+
<section className="mb-16">
|
|
72
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Typography</h2>
|
|
73
|
+
<div className="border-t border-[var(--border-primary)] divide-y divide-[var(--border-primary)]">
|
|
74
|
+
<div className="py-4 flex items-baseline justify-between">
|
|
75
|
+
<span className="text-display text-[var(--text-primary)]">Display</span>
|
|
76
|
+
<span className="text-xs text-[var(--text-muted)]">2.5rem / 500</span>
|
|
77
|
+
</div>
|
|
78
|
+
<div className="py-4 flex items-baseline justify-between">
|
|
79
|
+
<span className="text-heading-1 text-[var(--text-primary)]">Heading 1</span>
|
|
80
|
+
<span className="text-xs text-[var(--text-muted)]">1.75rem / 500</span>
|
|
81
|
+
</div>
|
|
82
|
+
<div className="py-4 flex items-baseline justify-between">
|
|
83
|
+
<span className="text-heading-2 text-[var(--text-primary)]">Heading 2</span>
|
|
84
|
+
<span className="text-xs text-[var(--text-muted)]">1.25rem / 500</span>
|
|
85
|
+
</div>
|
|
86
|
+
<div className="py-4 flex items-baseline justify-between">
|
|
87
|
+
<span className="text-body text-[var(--text-primary)]">Body Text</span>
|
|
88
|
+
<span className="text-xs text-[var(--text-muted)]">0.875rem</span>
|
|
89
|
+
</div>
|
|
90
|
+
<div className="py-4 flex items-baseline justify-between">
|
|
91
|
+
<span className="text-caption text-[var(--text-muted)]">CAPTION TEXT</span>
|
|
92
|
+
<span className="text-xs text-[var(--text-muted)]">0.75rem</span>
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
</section>
|
|
96
|
+
|
|
97
|
+
{/* Buttons */}
|
|
98
|
+
<section className="mb-16">
|
|
99
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Buttons</h2>
|
|
100
|
+
<div className="border-t border-[var(--border-primary)] py-6">
|
|
101
|
+
<div className="flex flex-wrap items-center gap-4 mb-6">
|
|
102
|
+
<Button variant="primary">Primary</Button>
|
|
103
|
+
<Button variant="secondary">Secondary</Button>
|
|
104
|
+
<Button variant="ghost">Ghost</Button>
|
|
105
|
+
<Button variant="danger">Danger</Button>
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex flex-wrap items-center gap-4">
|
|
108
|
+
<Button size="sm">Small</Button>
|
|
109
|
+
<Button size="md">Medium</Button>
|
|
110
|
+
<Button size="lg">Large</Button>
|
|
111
|
+
<Button disabled>Disabled</Button>
|
|
112
|
+
</div>
|
|
113
|
+
</div>
|
|
114
|
+
</section>
|
|
115
|
+
|
|
116
|
+
{/* Badges */}
|
|
117
|
+
<section className="mb-16">
|
|
118
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Badges</h2>
|
|
119
|
+
<div className="border-t border-[var(--border-primary)] py-6">
|
|
120
|
+
<div className="flex flex-wrap gap-3 mb-4">
|
|
121
|
+
<Badge>Default</Badge>
|
|
122
|
+
<Badge variant="success">Success</Badge>
|
|
123
|
+
<Badge variant="warning">Warning</Badge>
|
|
124
|
+
<Badge variant="danger">Danger</Badge>
|
|
125
|
+
<Badge variant="info">Info</Badge>
|
|
126
|
+
</div>
|
|
127
|
+
<div className="flex flex-wrap gap-3 mb-4">
|
|
128
|
+
<StatusBadge status="TODO" />
|
|
129
|
+
<StatusBadge status="IN_PROGRESS" />
|
|
130
|
+
<StatusBadge status="IN_REVIEW" />
|
|
131
|
+
<StatusBadge status="COMPLETE" />
|
|
132
|
+
</div>
|
|
133
|
+
<div className="flex flex-wrap gap-3">
|
|
134
|
+
<PriorityBadge priority="LOW" />
|
|
135
|
+
<PriorityBadge priority="MEDIUM" />
|
|
136
|
+
<PriorityBadge priority="HIGH" />
|
|
137
|
+
<PriorityBadge priority="CRITICAL" />
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
</section>
|
|
141
|
+
|
|
142
|
+
{/* Cards */}
|
|
143
|
+
<section className="mb-16">
|
|
144
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Cards</h2>
|
|
145
|
+
<div className="border border-[var(--border-primary)]">
|
|
146
|
+
<Card variant="bordered">
|
|
147
|
+
<CardHeader>
|
|
148
|
+
<h3 className="text-sm font-medium text-[var(--text-primary)]">Card Title</h3>
|
|
149
|
+
</CardHeader>
|
|
150
|
+
<CardContent>
|
|
151
|
+
<p className="text-sm text-[var(--text-secondary)]">
|
|
152
|
+
Minimal card with thin borders and no shadows.
|
|
153
|
+
</p>
|
|
154
|
+
</CardContent>
|
|
155
|
+
<CardFooter>
|
|
156
|
+
<div className="flex justify-end gap-3">
|
|
157
|
+
<Button variant="ghost" size="sm">Cancel</Button>
|
|
158
|
+
<Button size="sm">Save</Button>
|
|
159
|
+
</div>
|
|
160
|
+
</CardFooter>
|
|
161
|
+
</Card>
|
|
162
|
+
</div>
|
|
163
|
+
</section>
|
|
164
|
+
|
|
165
|
+
{/* Form Elements */}
|
|
166
|
+
<section className="mb-16">
|
|
167
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Form Elements</h2>
|
|
168
|
+
<div className="border-t border-[var(--border-primary)] py-6 space-y-6 max-w-md">
|
|
169
|
+
<Input label="Input" placeholder="Enter text..." />
|
|
170
|
+
<Input label="With Error" placeholder="Enter text..." error="This field has an error" />
|
|
171
|
+
<Select
|
|
172
|
+
label="Select"
|
|
173
|
+
value={selectValue}
|
|
174
|
+
onChange={setSelectValue}
|
|
175
|
+
placeholder="Choose option..."
|
|
176
|
+
options={[
|
|
177
|
+
{ value: '1', label: 'Option 1' },
|
|
178
|
+
{ value: '2', label: 'Option 2' },
|
|
179
|
+
]}
|
|
180
|
+
/>
|
|
181
|
+
<Textarea label="Textarea" placeholder="Write here..." rows={3} />
|
|
182
|
+
</div>
|
|
183
|
+
</section>
|
|
184
|
+
|
|
185
|
+
{/* Avatars */}
|
|
186
|
+
<section className="mb-16">
|
|
187
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Avatars</h2>
|
|
188
|
+
<div className="border-t border-[var(--border-primary)] py-6">
|
|
189
|
+
<div className="flex items-end gap-6">
|
|
190
|
+
<Avatar name="John Doe" size="sm" />
|
|
191
|
+
<Avatar name="Jane Smith" size="md" />
|
|
192
|
+
<Avatar name="Bob Wilson" size="lg" />
|
|
193
|
+
<Avatar name="PM" emoji="👔" size="md" />
|
|
194
|
+
</div>
|
|
195
|
+
</div>
|
|
196
|
+
</section>
|
|
197
|
+
|
|
198
|
+
{/* Modal */}
|
|
199
|
+
<section className="mb-16">
|
|
200
|
+
<h2 className="text-xs font-medium uppercase tracking-wider text-[var(--text-muted)] mb-6">Modal</h2>
|
|
201
|
+
<div className="border-t border-[var(--border-primary)] py-6">
|
|
202
|
+
<Button onClick={() => setModalOpen(true)}>Open Modal</Button>
|
|
203
|
+
</div>
|
|
204
|
+
</section>
|
|
205
|
+
</main>
|
|
206
|
+
|
|
207
|
+
<Modal isOpen={modalOpen} onClose={() => setModalOpen(false)} title="Modal Title">
|
|
208
|
+
<p className="text-sm text-[var(--text-secondary)] mb-6">
|
|
209
|
+
Minimal modal dialog with thin borders.
|
|
210
|
+
</p>
|
|
211
|
+
<div className="flex justify-end gap-3">
|
|
212
|
+
<Button variant="ghost" onClick={() => setModalOpen(false)}>Cancel</Button>
|
|
213
|
+
<Button onClick={() => setModalOpen(false)}>Confirm</Button>
|
|
214
|
+
</div>
|
|
215
|
+
</Modal>
|
|
216
|
+
|
|
217
|
+
{/* Footer */}
|
|
218
|
+
<footer className="border-t border-[var(--border-primary)] py-8">
|
|
219
|
+
<p className="text-center text-xs text-[var(--text-muted)]">
|
|
220
|
+
Olly Molly Design System
|
|
221
|
+
</p>
|
|
222
|
+
</footer>
|
|
223
|
+
</div>
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function ColorRow({ name, value, isText = false }: { name: string; value: string; isText?: boolean }) {
|
|
228
|
+
return (
|
|
229
|
+
<div className="flex items-center justify-between py-3 border-b border-[var(--border-primary)]">
|
|
230
|
+
<div className="flex items-center gap-4">
|
|
231
|
+
<div
|
|
232
|
+
className={`w-8 h-8 border border-[var(--border-primary)] ${isText ? 'flex items-center justify-center bg-[var(--bg-card)]' : ''}`}
|
|
233
|
+
style={{ backgroundColor: isText ? undefined : value }}
|
|
234
|
+
>
|
|
235
|
+
{isText && <span style={{ color: value }} className="text-sm font-medium">A</span>}
|
|
236
|
+
</div>
|
|
237
|
+
<span className="text-sm text-[var(--text-primary)]">{name}</span>
|
|
238
|
+
</div>
|
|
239
|
+
<span className="text-xs text-[var(--text-muted)] font-mono">{value}</span>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
}
|
package/app/favicon.ico
ADDED
|
Binary file
|