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
10 changes: 8 additions & 2 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

This file contains instructions for GitHub Copilot to follow when generating code for this project. Please adhere to these guidelines to ensure consistency and maintainability across the codebase. This project is CoRATES (Collaborative Research Appraisal Tool for Evidence Synthesis), a SolidJS-based web application deployed on Cloudflare Workers.

the /web package contains the frontend application built with SolidJS.
the /workers package contains backend services, including API endpoints and database migrations.
the /landing package contains the marketing and landing site.

The web package is copied into the landing package during the build process for deployment and all deployed as a single site on a single worker.

## Coding Standards

- Do not use emojis in code, comments, documentation, or commit messages.
Expand All @@ -26,15 +32,15 @@ This file contains instructions for GitHub Copilot to follow when generating cod
Use Zod for schema and input validation.
Use Drizzle ORM for database interactions and migrations.
Use Better-Auth for authentication and user management.
Use Zag.js for UI components and design system.

## Documentation Tool

PLEASE USE THE CORATES MCP tools to explore local documentation sources. Use this MCP for all Better-Auth, Drizzle, Icons, and Zag documentation.

## Zag.js

When you need to implement UI components use zag.js
Zag component exist in `packages/web/src/components/zag/*` and should be reused, see the README.md in that folder for a list of existing components.
Zag component exist in `packages/web/src/components/zag/*` and should be reused, see the README.md in that folder for a list of existing components. BE SURE TO CHECK THAT LIST AND REFERENCE EXISTING COMPONENTS AS WELL AS THE DOCS MCP BEFORE ADDING NEW COMPONENTS AND WHEN DEBUGGING.

## Additional References

Expand Down
1 change: 1 addition & 0 deletions packages/landing/public/site.webmanifest
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "CoRATES",
"short_name": "CoRATES",
"start_url": "/",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
Expand Down
1 change: 1 addition & 0 deletions packages/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"@zag-js/password-input": "^1.31.1",
"@zag-js/pin-input": "^1.31.1",
"@zag-js/qr-code": "^1.31.1",
"@zag-js/select": "^1.31.1",
"@zag-js/solid": "^1.31.1",
"@zag-js/splitter": "^1.31.1",
"@zag-js/switch": "^1.31.1",
Expand Down
1 change: 0 additions & 1 deletion packages/web/src/components/auth-ui/CheckEmail.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ export default function CheckEmail() {

const currentUser = user();


if (isAuthenticated() && currentUser?.emailVerified) {
if (checkInterval()) {
clearInterval(checkInterval());
Expand Down
255 changes: 255 additions & 0 deletions packages/web/src/components/project-ui/EditStudyModal.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
import { createSignal, createEffect, createMemo, Show } from 'solid-js';
import { Dialog } from '@components/zag/Dialog.jsx';
import { showToast } from '@components/zag/Toast.jsx';
import Select from '@components/zag/Select.jsx';
import Collapsible from '@components/zag/Collapsible.jsx';
import { BiRegularUser, BiRegularChevronDown } from 'solid-icons/bi';
import projectStore from '@/stores/projectStore.js';

/**
* EditStudyModal - Modal for editing study metadata and reviewer assignments
*
* @param {Object} props
* @param {boolean} props.open - Whether the modal is open
* @param {Function} props.onOpenChange - Callback when open state changes
* @param {Object} props.study - The study to edit
* @param {string} props.projectId - The project ID
* @param {Function} props.onUpdateStudy - Callback to update the study
*/
export default function EditStudyModal(props) {
const [name, setName] = createSignal('');
const [firstAuthor, setFirstAuthor] = createSignal('');
const [publicationYear, setPublicationYear] = createSignal('');
const [journal, setJournal] = createSignal('');
const [doi, setDoi] = createSignal('');
const [abstract, setAbstract] = createSignal('');
const [reviewer1, setReviewer1] = createSignal('');
const [reviewer2, setReviewer2] = createSignal('');
const [saving, setSaving] = createSignal(false);

const members = () => projectStore.getMembers(props.projectId) || [];

// Convert members to Select items format
const memberItems = createMemo(() => {
const getMemberName = member =>
member?.displayName || member?.name || member?.email || 'Unknown';

return [
{ label: 'Unassigned', value: '' },
...members().map(m => ({
label: getMemberName(m),
value: m.userId,
})),
];
});

// Reset form when study changes or modal opens
createEffect(() => {
const study = props.study;
if (study && props.open) {
setName(study.name || '');
setFirstAuthor(study.firstAuthor || '');
setPublicationYear(study.publicationYear?.toString() || '');
setJournal(study.journal || '');
setDoi(study.doi || '');
setAbstract(study.abstract || '');
setReviewer1(study.reviewer1 || '');
setReviewer2(study.reviewer2 || '');
}
});

const handleSave = async () => {
if (!props.study) return;

setSaving(true);
try {
// Validate publication year before passing to the update API.
// Only accept a finite integer within a reasonable range; otherwise leave undefined.
let parsedPublicationYear;
if (publicationYear()) {
const raw = publicationYear().toString().trim();
const num = Number(raw);
if (Number.isFinite(num)) {
const trunc = Math.trunc(num);
if (Number.isInteger(trunc) && trunc >= 1900 && trunc <= 2100) {
parsedPublicationYear = trunc;
}
}
}

const updates = {
name: name().trim() || props.study.name,
firstAuthor: firstAuthor().trim() || undefined,
publicationYear: parsedPublicationYear,
journal: journal().trim() || undefined,
doi: doi().trim() || undefined,
abstract: abstract().trim() || undefined,
reviewer1: reviewer1() || null,
reviewer2: reviewer2() || null,
};

await props.onUpdateStudy(props.study.id, updates);
props.onOpenChange(false);
} catch (err) {
console.error('Error updating study:', err);
showToast.error('Update Failed', 'Failed to update study.');
} finally {
setSaving(false);
}
};

return (
<Dialog open={props.open} onOpenChange={props.onOpenChange} title='Edit Study' size='lg'>
<div class='space-y-4'>
{/* Study Name */}
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>Study Name</label>
<input
type='text'
value={name()}
onInput={e => setName(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='Enter study name'
/>
</div>

{/* Citation Information - Collapsible */}
<div class='border-t border-gray-200 pt-4'>
<Collapsible
trigger={api => (
<button
{...api.getTriggerProps()}
class='flex items-center justify-between w-full text-left py-2 text-sm font-medium text-gray-900 hover:text-gray-700'
>
<span>Citation Information</span>
<BiRegularChevronDown
class={`w-5 h-5 text-gray-500 transition-transform ${api.open ? 'rotate-180' : ''}`}
/>
</button>
)}
>
<div class='space-y-4 pt-2'>
<div class='grid grid-cols-2 gap-4'>
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>First Author</label>
<input
type='text'
value={firstAuthor()}
onInput={e => setFirstAuthor(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='e.g., Smith'
/>
</div>
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>
Publication Year
</label>
<input
type='number'
value={publicationYear()}
onInput={e => setPublicationYear(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='e.g., 2024'
min='1900'
max='2100'
step='1'
inputMode='numeric'
/>
</div>
</div>

{/* Journal */}
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>Journal</label>
<input
type='text'
value={journal()}
onInput={e => setJournal(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='e.g., Journal of Clinical Research'
/>
</div>

{/* DOI */}
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>DOI</label>
<input
type='text'
value={doi()}
onInput={e => setDoi(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='e.g., 10.1000/xyz123'
/>
</div>

{/* Abstract */}
<div>
<label class='block text-sm font-medium text-gray-700 mb-1'>Abstract</label>
<textarea
value={abstract()}
onInput={e => setAbstract(e.target.value)}
class='w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500'
placeholder='Enter abstract...'
rows={2}
/>
</div>
</div>
</Collapsible>
</div>

{/* Reviewer Assignments */}
<div class='border-t border-gray-200 pt-4'>
<h3 class='text-sm font-medium text-gray-900 mb-3 flex items-center gap-2'>
<BiRegularUser class='w-4 h-4' />
Reviewer Assignments
</h3>
<div class='grid grid-cols-2 gap-4'>
<Select
label='Reviewer 1'
items={memberItems()}
value={reviewer1()}
onChange={setReviewer1}
placeholder='Unassigned'
disabledValues={reviewer2() ? [reviewer2()] : []}
inDialog
/>
<Select
label='Reviewer 2'
items={memberItems()}
value={reviewer2()}
onChange={setReviewer2}
placeholder='Unassigned'
disabledValues={reviewer1() ? [reviewer1()] : []}
inDialog
/>
</div>
<Show when={members().length === 0}>
<p class='text-sm text-gray-500 mt-2'>
No team members available. Add members to the project first.
</p>
</Show>
</div>

{/* Actions */}
<div class='flex justify-end gap-3 pt-4 border-t border-gray-200'>
<button
type='button'
onClick={() => props.onOpenChange(false)}
disabled={saving()}
class='px-4 py-2 text-sm font-medium text-gray-700 bg-white border border-gray-300 rounded-md hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50'
>
Cancel
</button>
<button
type='button'
onClick={handleSave}
disabled={saving()}
class='px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50'
>
{saving() ? 'Saving...' : 'Save Changes'}
</button>
</div>
</div>
</Dialog>
);
}
35 changes: 34 additions & 1 deletion packages/web/src/components/project-ui/tabs/AllStudiesTab.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { For, Show, createSignal, onMount } from 'solid-js';
import { AiOutlineBook, AiOutlineFileSync } from 'solid-icons/ai';
import { BiRegularEdit, BiRegularUpload } from 'solid-icons/bi';
import { CgFileDocument } from 'solid-icons/cg';
import { FiTrash2 } from 'solid-icons/fi';
import { FiTrash2, FiSettings } from 'solid-icons/fi';
import { FaBrandsGoogleDrive } from 'solid-icons/fa';
import AddStudiesForm from '../AddStudiesForm.jsx';
import GoogleDrivePickerModal from '../google-drive/GoogleDrivePickerModal.jsx';
import EditStudyModal from '../EditStudyModal.jsx';
import { showToast } from '@components/zag/Toast.jsx';
import projectStore from '@/stores/projectStore.js';
import { useProjectContext } from '../ProjectContext.jsx';
Expand All @@ -27,6 +28,8 @@ export default function AllStudiesTab() {
const [showGoogleDriveModal, setShowGoogleDriveModal] = createSignal(false);
const [googleDriveTargetStudyId, setGoogleDriveTargetStudyId] = createSignal(null);
const [restoredState, setRestoredState] = createSignal(null);
const [showEditModal, setShowEditModal] = createSignal(false);
const [editingStudy, setEditingStudy] = createSignal(null);

// Check for and restore state on mount (after OAuth redirect)
onMount(async () => {
Expand Down Expand Up @@ -74,6 +77,19 @@ export default function AllStudiesTab() {
const studyId = googleDriveTargetStudyId();
handlers.pdfHandlers.handleGoogleDriveImportSuccess(studyId, file);
};

const handleOpenEditModal = study => {
setEditingStudy(study);
setShowEditModal(true);
};

const handleCloseEditModal = open => {
if (!open) {
setShowEditModal(false);
setEditingStudy(null);
}
};

return (
<div class='space-y-4'>
{/* Add Studies Section - Unified form with PDF upload, reference import, and DOI lookup */}
Expand Down Expand Up @@ -344,6 +360,14 @@ export default function AllStudiesTab() {
<FaBrandsGoogleDrive class='w-3.5 h-3.5' />
</button>
</Show>
{/* Edit button */}
<button
onClick={() => handleOpenEditModal(study)}
class='p-1 text-gray-400 hover:text-blue-600 hover:bg-blue-50 rounded transition-colors'
title='Edit study metadata and reviewers'
>
<FiSettings class='w-4 h-4' />
</button>
{/* Delete button */}
<button
onClick={() => handlers.studyHandlers.handleDeleteStudy(study.id)}
Expand Down Expand Up @@ -371,6 +395,15 @@ export default function AllStudiesTab() {
studyId={googleDriveTargetStudyId()}
onImportSuccess={handleGoogleDriveImportSuccess}
/>

{/* Edit Study Modal */}
<EditStudyModal
open={showEditModal()}
onOpenChange={handleCloseEditModal}
study={editingStudy()}
projectId={projectId}
onUpdateStudy={handlers.studyHandlers.handleUpdateStudy}
/>
</div>
);
}
Loading