Skip to content

Commit 03a5105

Browse files
feat: Add detail pages for all search result types
All search result types now have working detail pages: - Complex, Set, Pathway, and DNA Sequence use the existing detail page - Interactor results navigate to a new interactor detail page with summary info, synonyms, and interaction partners with Reactome entities - Icon results navigate to a new icon detail page with SVG preview, credits, references, and associated physical entities - Search links are now type-aware, routing each type to its correct page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 8635db6 commit 03a5105

10 files changed

Lines changed: 822 additions & 3 deletions

File tree

projects/website-angular/src/app/app.routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ export const routes: Routes = [
3737
{ path: 'content/schema/:className', loadComponent: () => import('./content/schema/schema.component').then(m => m.SchemaComponent), pathMatch: 'full' },
3838

3939
//Detail Pages
40+
{ path: 'content/detail/interactor/:acc', loadComponent: () => import('./content/detail/interactor-detail/interactor-detail.component').then(m => m.InteractorDetailComponent) },
41+
{ path: 'content/detail/icon/:id', loadComponent: () => import('./content/detail/icon-detail/icon-detail.component').then(m => m.IconDetailComponent) },
4042
{ path: 'content/detail/:id', loadComponent: () => import('./content/detail/detail.component').then(m => m.DetailComponent) },
4143

4244
//Search Pages
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
<app-page-layout [showSidebar]="false" [showBreadcrumb]="false">
2+
@if (loading()) {
3+
<div class="loading-container">
4+
<mat-spinner diameter="48"></mat-spinner>
5+
</div>
6+
} @else if (error()) {
7+
<div class="error-container">
8+
<h2>Icon not found</h2>
9+
<p>The requested icon could not be found.</p>
10+
</div>
11+
} @else if (icon()) {
12+
<div class="icon-detail">
13+
<app-tile variant="light">
14+
<div class="icon-header">
15+
<div class="icon-info">
16+
<h1>{{ icon()!.name }}</h1>
17+
@if (icon()!.iconCategories?.length) {
18+
<div class="icon-categories">
19+
@for (cat of icon()!.iconCategories; track cat) {
20+
<span class="category-tag">{{ cat }}</span>
21+
}
22+
</div>
23+
}
24+
</div>
25+
<div class="icon-preview">
26+
<img [src]="getSvgUrl()" [alt]="icon()!.name" />
27+
</div>
28+
</div>
29+
</app-tile>
30+
31+
@if (icon()!.summation) {
32+
<app-tile variant="light">
33+
<h2>Description</h2>
34+
<p [innerHTML]="icon()!.summation"></p>
35+
</app-tile>
36+
}
37+
38+
@if (icon()!.iconCuratorName || icon()!.iconDesignerName) {
39+
<app-tile variant="light">
40+
<h2>Credits</h2>
41+
<div class="credits">
42+
@if (icon()!.iconCuratorName) {
43+
<div class="credit-item">
44+
<span class="credit-label">Curator:</span>
45+
@if (icon()!.iconCuratorOrcidId) {
46+
<a [href]="'https://orcid.org/' + icon()!.iconCuratorOrcidId" target="_blank" rel="noopener">
47+
{{ icon()!.iconCuratorName }}
48+
<span class="material-symbols-rounded">open_in_new</span>
49+
</a>
50+
} @else {
51+
<span>{{ icon()!.iconCuratorName }}</span>
52+
}
53+
</div>
54+
}
55+
@if (icon()!.iconDesignerName) {
56+
<div class="credit-item">
57+
<span class="credit-label">Designer:</span>
58+
@if (icon()!.iconDesignerUrl) {
59+
<a [href]="icon()!.iconDesignerUrl" target="_blank" rel="noopener">
60+
{{ icon()!.iconDesignerName }}
61+
<span class="material-symbols-rounded">open_in_new</span>
62+
</a>
63+
} @else {
64+
<span>{{ icon()!.iconDesignerName }}</span>
65+
}
66+
</div>
67+
}
68+
</div>
69+
</app-tile>
70+
}
71+
72+
@if (getUniProtReferences().length) {
73+
<app-tile variant="light">
74+
<h2>External References</h2>
75+
<div class="references">
76+
@for (ref of getUniProtReferences(); track ref) {
77+
<a [href]="'https://www.uniprot.org/uniprot/' + ref" target="_blank" rel="noopener" class="ref-link">
78+
UniProt: {{ ref }}
79+
<span class="material-symbols-rounded">open_in_new</span>
80+
</a>
81+
}
82+
</div>
83+
</app-tile>
84+
}
85+
86+
@if (icon()!.iconPhysicalEntities?.length) {
87+
<app-tile variant="light">
88+
<h2>Physical Entities ({{ icon()!.iconPhysicalEntities.length }})</h2>
89+
<div class="table-wrapper">
90+
<table class="entities-table">
91+
<thead>
92+
<tr>
93+
<th>Name</th>
94+
<th>Identifier</th>
95+
<th>Type</th>
96+
<th>Compartment</th>
97+
</tr>
98+
</thead>
99+
<tbody>
100+
@for (entity of icon()!.iconPhysicalEntities; track entity.stId) {
101+
<tr>
102+
<td>
103+
<a [routerLink]="'/content/detail/' + entity.stId">{{ entity.displayName || entity.name }}</a>
104+
</td>
105+
<td>{{ entity.stId }}</td>
106+
<td>{{ entity.type }}</td>
107+
<td>{{ entity.compartments }}</td>
108+
</tr>
109+
}
110+
</tbody>
111+
</table>
112+
</div>
113+
</app-tile>
114+
}
115+
</div>
116+
}
117+
</app-page-layout>
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
:host {
2+
display: block;
3+
}
4+
5+
.loading-container {
6+
display: flex;
7+
justify-content: center;
8+
align-items: center;
9+
min-height: 300px;
10+
}
11+
12+
.error-container {
13+
display: flex;
14+
flex-direction: column;
15+
align-items: center;
16+
justify-content: center;
17+
min-height: 300px;
18+
color: var(--on-surface);
19+
20+
h2 {
21+
margin-bottom: 8px;
22+
}
23+
24+
p {
25+
color: var(--outline);
26+
}
27+
}
28+
29+
.icon-detail {
30+
display: flex;
31+
flex-direction: column;
32+
gap: 16px;
33+
}
34+
35+
.icon-header {
36+
display: flex;
37+
align-items: flex-start;
38+
justify-content: space-between;
39+
gap: 24px;
40+
flex-wrap: wrap;
41+
42+
.icon-info {
43+
flex: 1;
44+
min-width: 200px;
45+
46+
h1 {
47+
margin: 0 0 12px;
48+
font-size: 1.5rem;
49+
}
50+
}
51+
52+
.icon-preview {
53+
flex-shrink: 0;
54+
width: 120px;
55+
height: 120px;
56+
display: flex;
57+
align-items: center;
58+
justify-content: center;
59+
border: 1px solid var(--outline, #ddd);
60+
border-radius: 8px;
61+
padding: 12px;
62+
background: white;
63+
64+
img {
65+
max-width: 100%;
66+
max-height: 100%;
67+
object-fit: contain;
68+
}
69+
}
70+
}
71+
72+
.icon-categories {
73+
display: flex;
74+
flex-wrap: wrap;
75+
gap: 8px;
76+
}
77+
78+
.category-tag {
79+
display: inline-block;
80+
padding: 4px 12px;
81+
border-radius: 16px;
82+
background: var(--surface, #f0f0f0);
83+
font-size: 0.85rem;
84+
text-transform: capitalize;
85+
}
86+
87+
h2 {
88+
margin: 0 0 12px;
89+
font-size: 1.2rem;
90+
}
91+
92+
.credits {
93+
display: flex;
94+
flex-direction: column;
95+
gap: 8px;
96+
}
97+
98+
.credit-item {
99+
display: flex;
100+
align-items: center;
101+
gap: 8px;
102+
103+
.credit-label {
104+
font-weight: 600;
105+
}
106+
107+
a {
108+
display: inline-flex;
109+
align-items: center;
110+
gap: 4px;
111+
color: var(--primary);
112+
text-decoration: none;
113+
114+
&:hover {
115+
text-decoration: underline;
116+
}
117+
118+
.material-symbols-rounded {
119+
font-size: 16px;
120+
}
121+
}
122+
}
123+
124+
.references {
125+
display: flex;
126+
flex-direction: column;
127+
gap: 8px;
128+
}
129+
130+
.ref-link {
131+
display: inline-flex;
132+
align-items: center;
133+
gap: 4px;
134+
color: var(--primary);
135+
text-decoration: none;
136+
137+
&:hover {
138+
text-decoration: underline;
139+
}
140+
141+
.material-symbols-rounded {
142+
font-size: 16px;
143+
}
144+
}
145+
146+
.table-wrapper {
147+
overflow-x: auto;
148+
}
149+
150+
.entities-table {
151+
width: 100%;
152+
border-collapse: collapse;
153+
154+
th, td {
155+
padding: 10px 14px;
156+
text-align: left;
157+
border-bottom: 1px solid var(--outline, #ddd);
158+
}
159+
160+
th {
161+
font-weight: 600;
162+
background: var(--surface, #f5f5f5);
163+
white-space: nowrap;
164+
}
165+
166+
tbody tr:hover {
167+
background: rgba(0, 0, 0, 0.02);
168+
}
169+
170+
a {
171+
color: var(--primary);
172+
text-decoration: none;
173+
174+
&:hover {
175+
text-decoration: underline;
176+
}
177+
}
178+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import {Component, inject, OnInit, signal} from '@angular/core';
2+
import {ActivatedRoute, RouterLink} from '@angular/router';
3+
import {MatProgressSpinner} from '@angular/material/progress-spinner';
4+
import {PageLayoutComponent} from '../../../page-layout/page-layout.component';
5+
import {TileComponent} from '../../../reactome-components/tile/tile.component';
6+
import {IconService, IconEntry} from '../../../../services/icon.service';
7+
8+
@Component({
9+
selector: 'app-icon-detail',
10+
standalone: true,
11+
imports: [PageLayoutComponent, TileComponent, MatProgressSpinner, RouterLink],
12+
templateUrl: './icon-detail.component.html',
13+
styleUrl: './icon-detail.component.scss',
14+
})
15+
export class IconDetailComponent implements OnInit {
16+
private route = inject(ActivatedRoute);
17+
private iconService = inject(IconService);
18+
19+
icon = signal<IconEntry | null>(null);
20+
loading = signal(true);
21+
error = signal(false);
22+
23+
ngOnInit() {
24+
const id = this.route.snapshot.paramMap.get('id');
25+
if (!id) {
26+
this.loading.set(false);
27+
this.error.set(true);
28+
return;
29+
}
30+
31+
this.iconService.getIcon(id).subscribe({
32+
next: (data) => {
33+
this.icon.set(data);
34+
this.loading.set(false);
35+
},
36+
error: () => {
37+
this.error.set(true);
38+
this.loading.set(false);
39+
},
40+
});
41+
}
42+
43+
getSvgUrl(): string {
44+
const stId = this.icon()?.stId;
45+
return stId ? `https://reactome.org/icon/${stId}.svg` : '';
46+
}
47+
48+
getUniProtReferences(): string[] {
49+
const refs = this.icon()?.iconReferences ?? [];
50+
return refs.filter(r => !r.includes(':'));
51+
}
52+
}

0 commit comments

Comments
 (0)