Merge dev-2026-03-22 into main #10
45
src/components/CopyableCode.tsx
Normal file
45
src/components/CopyableCode.tsx
Normal file
@@ -0,0 +1,45 @@
|
||||
import { useState } from 'react'
|
||||
|
||||
interface Props {
|
||||
code: string
|
||||
prefix?: string
|
||||
}
|
||||
|
||||
export default function CopyableCode({ code, prefix }: Props) {
|
||||
const [copied, setCopied] = useState(false)
|
||||
|
||||
const handleCopy = async (e: React.MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
try {
|
||||
await navigator.clipboard.writeText(code)
|
||||
setCopied(true)
|
||||
setTimeout(() => setCopied(false), 1500)
|
||||
} catch {
|
||||
// fallback: select text
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<span
|
||||
className="copyable-code"
|
||||
title="Click to copy code"
|
||||
onClick={handleCopy}
|
||||
style={{
|
||||
cursor: 'pointer',
|
||||
fontFamily: 'monospace',
|
||||
padding: '2px 8px',
|
||||
borderRadius: '4px',
|
||||
background: 'var(--bg-hover, rgba(255,255,255,.06))',
|
||||
border: '1px solid var(--border, rgba(255,255,255,.1))',
|
||||
fontSize: '0.95em',
|
||||
userSelect: 'all',
|
||||
transition: 'background .15s',
|
||||
}}
|
||||
>
|
||||
{prefix}{code}
|
||||
{copied && (
|
||||
<span style={{ marginLeft: 6, fontSize: '0.8em', color: 'var(--success, #10b981)' }}>✓</span>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import { useParams, useNavigate } from 'react-router-dom'
|
||||
import api from '@/services/api'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import dayjs from 'dayjs'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
interface MeetingItem {
|
||||
id: number
|
||||
@@ -153,7 +154,7 @@ export default function MeetingDetailPage() {
|
||||
<button className="btn-back" onClick={() => navigate(-1)}>← Back</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>📅 {meeting.meeting_code || `#${meeting.id}`}</h2>
|
||||
<h2>📅 {meeting.meeting_code ? <CopyableCode code={meeting.meeting_code} /> : `#${meeting.id}`}</h2>
|
||||
<div className="task-meta">
|
||||
<span className={`badge status-${meeting.status}`}>{meeting.status}</span>
|
||||
{meeting.project_code && <span className="text-dim">Project: {meeting.project_code}</span>}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
|
||||
import CreateTaskModal from '@/components/CreateTaskModal'
|
||||
import MilestoneFormModal from '@/components/MilestoneFormModal'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
interface MilestoneTask {
|
||||
id: number
|
||||
@@ -155,7 +156,7 @@ export default function MilestoneDetailPage() {
|
||||
<button className="btn-back" onClick={() => navigate('/milestones')}>← Back to Milestones</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>🏁 {milestone.title}</h2>
|
||||
<h2>🏁 {milestone.milestone_code && <><CopyableCode code={milestone.milestone_code} /> </>}{milestone.title}</h2>
|
||||
<div className="task-meta">
|
||||
<span className={`badge status-${milestone.status}`}>{milestone.status}</span>
|
||||
{milestone.due_date && <span className="text-dim">Due {dayjs(milestone.due_date).format('YYYY-MM-DD')}</span>}
|
||||
|
||||
@@ -6,6 +6,7 @@ import dayjs from 'dayjs'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import ProjectFormModal from '@/components/ProjectFormModal'
|
||||
import MilestoneFormModal from '@/components/MilestoneFormModal'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
export default function ProjectDetailPage() {
|
||||
const { id } = useParams()
|
||||
@@ -74,7 +75,7 @@ export default function ProjectDetailPage() {
|
||||
<button className="btn-back" onClick={() => navigate('/projects')}>← Back to projects</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>📁 {project.name} {project.project_code && <span className="badge">{project.project_code}</span>}</h2>
|
||||
<h2>📁 {project.name} {project.project_code && <CopyableCode code={project.project_code} />}</h2>
|
||||
<p style={{ color: 'var(--text-dim)', marginTop: 4 }}>{project.description || 'No description'}</p>
|
||||
{project.repo && <p style={{ color: 'var(--text-dim)', marginTop: 4 }}>📦 {project.repo}</p>}
|
||||
<div className="text-dim">Owner: {project.owner_name || 'Unknown'}</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useParams, useNavigate, useSearchParams } from 'react-router-dom'
|
||||
import api from '@/services/api'
|
||||
import type { Propose, Milestone } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
export default function ProposeDetailPage() {
|
||||
const { id } = useParams()
|
||||
@@ -124,7 +125,7 @@ export default function ProposeDetailPage() {
|
||||
<div className="task-header">
|
||||
<h2>
|
||||
💡 {propose.title}
|
||||
{propose.propose_code && <span className="badge" style={{ marginLeft: 8 }}>{propose.propose_code}</span>}
|
||||
{propose.propose_code && <span style={{ marginLeft: 8 }}><CopyableCode code={propose.propose_code} /></span>}
|
||||
</h2>
|
||||
<span className={`badge ${statusBadgeClass(propose.status)}`} style={{ fontSize: '1rem' }}>
|
||||
{propose.status}
|
||||
@@ -136,7 +137,7 @@ export default function ProposeDetailPage() {
|
||||
<div className="section">
|
||||
<h3>Details</h3>
|
||||
<div className="detail-grid">
|
||||
<div><strong>Propose Code:</strong> {propose.propose_code || '—'}</div>
|
||||
<div><strong>Propose Code:</strong> {propose.propose_code ? <CopyableCode code={propose.propose_code} /> : '—'}</div>
|
||||
<div><strong>Status:</strong> {propose.status}</div>
|
||||
<div><strong>Created By:</strong> {propose.created_by_username || (propose.created_by_id ? `User #${propose.created_by_id}` : '—')}</div>
|
||||
<div><strong>Created:</strong> {dayjs(propose.created_at).format('YYYY-MM-DD HH:mm')}</div>
|
||||
|
||||
@@ -2,6 +2,7 @@ import { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import api from '@/services/api'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
interface SupportItem {
|
||||
id: number
|
||||
@@ -141,7 +142,7 @@ export default function SupportDetailPage() {
|
||||
<button className="btn-back" onClick={() => navigate(-1)}>← Back</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>🎫 {support.support_code || `#${support.id}`}</h2>
|
||||
<h2>🎫 {support.support_code ? <CopyableCode code={support.support_code} /> : `#${support.id}`}</h2>
|
||||
<div className="task-meta">
|
||||
<span className={`badge status-${support.status}`}>{support.status}</span>
|
||||
<span className={`badge priority-${support.priority}`}>{support.priority}</span>
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { Task, Comment, Project, ProjectMember, Milestone } from '@/types'
|
||||
import dayjs from 'dayjs'
|
||||
import { useAuth } from '@/hooks/useAuth'
|
||||
import CreateTaskModal from '@/components/CreateTaskModal'
|
||||
import CopyableCode from '@/components/CopyableCode'
|
||||
|
||||
export default function TaskDetailPage() {
|
||||
const { id } = useParams()
|
||||
@@ -120,7 +121,7 @@ export default function TaskDetailPage() {
|
||||
<button className="btn-back" onClick={() => navigate('/tasks')}>← Back</button>
|
||||
|
||||
<div className="task-header">
|
||||
<h2>{task.task_code ? `[${task.task_code}]` : `#${task.id}`} {task.title}</h2>
|
||||
<h2>{task.task_code ? <><CopyableCode code={task.task_code} /> </> : `#${task.id} `}{task.title}</h2>
|
||||
<div className="task-meta">
|
||||
<span className={`badge status-${task.status}`}>{task.status}</span>
|
||||
<span className={`badge priority-${task.priority}`}>{task.priority}</span>
|
||||
|
||||
Reference in New Issue
Block a user