Skip to content
This repository was archived by the owner on Apr 24, 2026. It is now read-only.

Commit ebe6cd6

Browse files
authored
Merge pull request #22 from renderghost/feature/issue-21-doi-type-source-of-truth
Enhancement #21: Use DOI type as source of truth
2 parents 337a3d9 + e8cc75a commit ebe6cd6

6 files changed

Lines changed: 58 additions & 105 deletions

File tree

lexicons/link/work/work.json

Lines changed: 2 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,8 @@
1616
},
1717
"type": {
1818
"type": "string",
19-
"enum": [
20-
"abstract",
21-
"poster",
22-
"paper",
23-
"conference-proceeding",
24-
"journal-article",
25-
"book-chapter",
26-
"book",
27-
"preprint",
28-
"dataset",
29-
"other"
30-
],
31-
"description": "Type of scholarly work"
19+
"maxLength": 100,
20+
"description": "Type of scholarly work (from CrossRef metadata)"
3221
},
3322
"title": {
3423
"type": "string",

src/app/api/profile/works/route.ts

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ export async function POST(request: NextRequest) {
1111
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
1212
}
1313

14-
const { doi, type, bypassDuplicateCheck } = await request.json();
14+
const { doi, bypassDuplicateCheck } = await request.json();
1515

1616
const repo = new ProfileRepository(agent);
1717

@@ -36,7 +36,7 @@ export async function POST(request: NextRequest) {
3636

3737
const work = {
3838
doi,
39-
type,
39+
type: metadata?.type || 'other',
4040
title: metadata?.title,
4141
authors: metadata?.authors,
4242
publicationDate: metadata?.publicationDate,
@@ -57,31 +57,13 @@ export async function POST(request: NextRequest) {
5757
}
5858
}
5959

60-
export async function PUT(request: NextRequest) {
61-
try {
62-
const agent = await getAgent();
63-
64-
if (!agent) {
65-
return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
66-
}
67-
68-
const { rkey, type } = await request.json();
69-
70-
if (!rkey) {
71-
return NextResponse.json({ error: 'rkey is required' }, { status: 400 });
72-
}
73-
74-
const repo = new ProfileRepository(agent);
75-
await repo.updateWork(rkey, { type });
76-
77-
return NextResponse.json({ success: true });
78-
} catch (error) {
79-
console.error('Error updating work:', error);
80-
return NextResponse.json(
81-
{ error: 'Failed to update work' },
82-
{ status: 500 }
83-
);
84-
}
60+
export async function PUT(_request: NextRequest) {
61+
// Works are immutable - DOI and type come from CrossRef metadata
62+
// This endpoint is kept for future extensibility
63+
return NextResponse.json(
64+
{ error: 'Works cannot be updated. Delete and re-add instead.' },
65+
{ status: 400 }
66+
);
8567
}
8668

8769
export async function DELETE(request: NextRequest) {

src/app/dashboard/research/page.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { redirect } from 'next/navigation';
22
import { getSession } from '@/lib/auth/session';
33
import { getAgent } from '@/lib/auth/atproto';
44
import { ProfileRepository } from '@/lib/data/repository';
5+
import { formatWorkType } from '@/lib/utils';
56
import Link from 'next/link';
67

78
export default async function ResearchPage() {
@@ -112,8 +113,8 @@ export default async function ResearchPage() {
112113
</p>
113114
)}
114115
<div className="flex flex-wrap gap-2 items-center">
115-
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded capitalize">
116-
{work.type.replace(/-/g, ' ')}
116+
<span className="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded">
117+
{formatWorkType(work.type)}
117118
</span>
118119
{work.venue && (
119120
<span className="text-xs text-gray-600">

src/components/research/ResearchForm.tsx

Lines changed: 31 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,33 @@
11
'use client';
22

3-
import { useState, useEffect } from 'react';
3+
import { useState } from 'react';
44
import { useRouter } from 'next/navigation';
5-
import type { LinkWork, WorkType } from '@/types';
5+
import type { LinkWork } from '@/types';
6+
import { formatWorkType } from '@/lib/utils';
67

78
interface ResearchFormProps {
89
mode: 'create' | 'edit';
910
initialData?: LinkWork & { rkey?: string };
1011
}
1112

12-
const WORK_TYPES: { value: WorkType; label: string }[] = [
13-
{ value: 'abstract', label: 'Abstract' },
14-
{ value: 'poster', label: 'Poster' },
15-
{ value: 'paper', label: 'Paper' },
16-
{ value: 'conference-proceeding', label: 'Conference Proceeding' },
17-
{ value: 'journal-article', label: 'Journal Article' },
18-
{ value: 'book-chapter', label: 'Book Chapter' },
19-
{ value: 'book', label: 'Book' },
20-
{ value: 'preprint', label: 'Preprint' },
21-
{ value: 'dataset', label: 'Dataset' },
22-
{ value: 'other', label: 'Other' },
23-
];
24-
2513
export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
2614
const router = useRouter();
2715
const [loading, setLoading] = useState(false);
2816
const [resolvingDOI, setResolvingDOI] = useState(false);
2917
const [error, setError] = useState('');
30-
const [resolvedMetadata, setResolvedMetadata] = useState<any>(null);
18+
const [resolvedMetadata, setResolvedMetadata] = useState<{
19+
title?: string;
20+
authors?: string[];
21+
journal?: string;
22+
publicationDate?: string;
23+
type?: string;
24+
abstract?: string;
25+
url?: string;
26+
} | null>(null);
3127
const [showDuplicateWarning, setShowDuplicateWarning] = useState(false);
3228

33-
const [formData, setFormData] = useState<{
34-
doi: string;
35-
type: WorkType | '';
36-
rkey?: string;
37-
}>({
29+
const [formData, setFormData] = useState({
3830
doi: initialData?.doi || '',
39-
type: initialData?.type || '',
4031
rkey: initialData?.rkey,
4132
});
4233

@@ -72,12 +63,10 @@ export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
7263
setShowDuplicateWarning(false);
7364

7465
try {
75-
const payload = mode === 'create'
76-
? { doi: formData.doi, type: formData.type, bypassDuplicateCheck }
77-
: { rkey: formData.rkey, type: formData.type };
66+
const payload = { doi: formData.doi, bypassDuplicateCheck };
7867

7968
const response = await fetch('/api/profile/works', {
80-
method: mode === 'create' ? 'POST' : 'PUT',
69+
method: 'POST',
8170
headers: {
8271
'Content-Type': 'application/json',
8372
},
@@ -201,7 +190,7 @@ export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
201190
)}
202191
{resolvedMetadata.type && (
203192
<p className="text-sm text-blue-800 mb-1">
204-
<strong>Type:</strong> {resolvedMetadata.type}
193+
<strong>Type:</strong> {formatWorkType(resolvedMetadata.type)}
205194
</p>
206195
)}
207196
{resolvedMetadata.abstract && (
@@ -250,6 +239,11 @@ export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
250239
<strong>Published:</strong> {new Date(initialData.publicationDate).getFullYear()}
251240
</p>
252241
)}
242+
{initialData.type && (
243+
<p className="text-sm text-gray-800 mb-1">
244+
<strong>Type:</strong> {formatWorkType(initialData.type)}
245+
</p>
246+
)}
253247
{('abstract' in initialData && initialData.abstract && typeof initialData.abstract === 'string') ? (
254248
<p className="text-sm text-gray-800 mb-1">
255249
<strong>Abstract:</strong> {initialData.abstract.substring(0, 200)}{initialData.abstract.length > 200 ? '...' : ''}
@@ -270,27 +264,6 @@ export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
270264
) : null}
271265
</div>
272266
)}
273-
274-
<div>
275-
<label className="block text-sm font-medium text-gray-700 mb-2">
276-
Type *
277-
</label>
278-
<select
279-
value={formData.type}
280-
onChange={(e) =>
281-
setFormData({ ...formData, type: e.target.value as WorkType })
282-
}
283-
required
284-
className="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent"
285-
>
286-
<option value="">Select type...</option>
287-
{WORK_TYPES.map((type) => (
288-
<option key={type.value} value={type.value}>
289-
{type.label}
290-
</option>
291-
))}
292-
</select>
293-
</div>
294267
</div>
295268

296269
{error && (
@@ -310,27 +283,23 @@ export default function ResearchForm({ mode, initialData }: ResearchFormProps) {
310283
)}
311284

312285
<div className="flex flex-col gap-3 mt-6">
313-
<button
314-
type="submit"
315-
disabled={loading}
316-
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
317-
>
318-
{loading
319-
? mode === 'create'
320-
? 'Adding...'
321-
: 'Saving...'
322-
: mode === 'create'
323-
? 'Add Research'
324-
: 'Save Changes'}
325-
</button>
286+
{mode === 'create' && (
287+
<button
288+
type="submit"
289+
disabled={loading}
290+
className="w-full bg-blue-600 text-white py-3 px-6 rounded-lg font-medium hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors"
291+
>
292+
{loading ? 'Adding...' : 'Add Research'}
293+
</button>
294+
)}
326295

327296
<button
328297
type="button"
329298
onClick={() => router.push('/dashboard/research')}
330299
disabled={loading}
331300
className="w-full py-3 px-6 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
332301
>
333-
Cancel
302+
{mode === 'edit' ? 'Back' : 'Cancel'}
334303
</button>
335304

336305
{mode === 'edit' && (

src/lib/utils.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,15 @@ import { twMerge } from 'tailwind-merge';
88
export function cn(...inputs: ClassValue[]) {
99
return twMerge(clsx(inputs));
1010
}
11+
12+
/**
13+
* Formats a work type string for human-readable display
14+
* Converts hyphenated lowercase to Title Case
15+
* Example: 'journal-article' → 'Journal Article'
16+
*/
17+
export function formatWorkType(type: string): string {
18+
return type
19+
.split('-')
20+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
21+
.join(' ');
22+
}

src/types/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ export interface LinkEvent {
116116

117117
export interface LinkWork {
118118
doi: string;
119-
type: 'abstract' | 'poster' | 'paper' | 'conference-proceeding' | 'journal-article' | 'book-chapter' | 'book' | 'preprint' | 'dataset' | 'other';
119+
type: string;
120120
title?: string;
121121
authors?: string[];
122122
publicationDate?: string;

0 commit comments

Comments
 (0)