Skip to content

Commit 489083d

Browse files
committed
feat(web): replace explore page placeholder with featured collections (task-1)
- Created FeaturedCollections component with 8 curated academic topics - Created TrendingTopics component showing most-searched terms from activity - Created RandomExplorer component with "Surprise Me" functionality - Updated explore route to display all three components - Added navigation to search page from collection topics - Integrated with activity context for trending data
1 parent c68dcc6 commit 489083d

File tree

5 files changed

+442
-13
lines changed

5 files changed

+442
-13
lines changed
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
/**
2+
* Featured Collections Component
3+
*
4+
* Displays curated collections of academic topics for exploration.
5+
* Each collection links to a pre-configured search query.
6+
*/
7+
8+
import { Button, Card, Container, SimpleGrid, Stack, Text, Title, useMantineTheme } from '@mantine/core';
9+
import { IconBook, IconBrain, IconBuilding, IconDatabase, IconDeviceDesktop, IconFlask, IconGlobe, IconHistory } from '@tabler/icons-react';
10+
import { useNavigate } from '@tanstack/react-router';
11+
12+
interface FeaturedCollection {
13+
id: string;
14+
title: string;
15+
description: string;
16+
query: string;
17+
icon: React.ReactNode;
18+
color: string;
19+
}
20+
21+
const FEATURED_COLLECTIONS: FeaturedCollection[] = [
22+
{
23+
id: 'machine-learning',
24+
title: 'Machine Learning',
25+
description: 'Explore research on neural networks, deep learning, and AI systems',
26+
query: 'machine learning',
27+
icon: <IconBrain size={32} />,
28+
color: 'blue',
29+
},
30+
{
31+
id: 'cultural-heritage',
32+
title: 'Cultural Heritage',
33+
description: 'Digital preservation, citizen science, and heritage engagement',
34+
query: 'cultural heritage preservation',
35+
icon: <IconHistory size={32} />,
36+
color: 'orange',
37+
},
38+
{
39+
id: 'climate-science',
40+
title: 'Climate Science',
41+
description: 'Climate change, environmental science, and sustainability research',
42+
query: 'climate change',
43+
icon: <IconGlobe size={32} />,
44+
color: 'green',
45+
},
46+
{
47+
id: 'bioinformatics',
48+
title: 'Bioinformatics',
49+
description: 'Computational biology, genomics, and biomedical research',
50+
query: 'bioinformatics',
51+
icon: <IconFlask size={32} />,
52+
color: 'grape',
53+
},
54+
{
55+
id: 'software-engineering',
56+
title: 'Software Engineering',
57+
description: 'Software development, programming languages, and systems',
58+
query: 'software engineering',
59+
icon: <IconDeviceDesktop size={32} />,
60+
color: 'cyan',
61+
},
62+
{
63+
id: 'data-science',
64+
title: 'Data Science',
65+
description: 'Big data, analytics, visualization, and statistical methods',
66+
query: 'data science',
67+
icon: <IconDatabase size={32} />,
68+
color: 'indigo',
69+
},
70+
{
71+
id: 'academic-institutions',
72+
title: 'Academic Institutions',
73+
description: 'Universities, research centers, and academic collaboration',
74+
query: 'university research',
75+
icon: <IconBuilding size={32} />,
76+
color: 'gray',
77+
},
78+
{
79+
id: 'open-science',
80+
title: 'Open Science',
81+
description: 'Open access, open data, and reproducible research practices',
82+
query: 'open science',
83+
icon: <IconBook size={32} />,
84+
color: 'teal',
85+
},
86+
];
87+
88+
export const FeaturedCollections: React.FC = () => {
89+
const navigate = useNavigate();
90+
const theme = useMantineTheme();
91+
92+
const handleExploreCollection = (query: string) => {
93+
navigate({
94+
to: '/search',
95+
search: { q: query, filter: undefined, search: undefined },
96+
});
97+
};
98+
99+
return (
100+
<Container size="xl">
101+
<Stack gap="lg">
102+
<div>
103+
<Title order={3}>Featured Collections</Title>
104+
<Text c="dimmed">Curated topics to kickstart your exploration</Text>
105+
</div>
106+
107+
<SimpleGrid cols={{ base: 1, sm: 2, lg: 4 }} spacing="md">
108+
{FEATURED_COLLECTIONS.map((collection) => (
109+
<Card
110+
key={collection.id}
111+
shadow="sm"
112+
padding="lg"
113+
radius="md"
114+
withBorder
115+
style={{ borderColor: theme.colors[collection.color][4] }}
116+
h="100%"
117+
>
118+
<Stack gap="md" h="100%">
119+
<div style={{ color: theme.colors[collection.color][6] }}>
120+
{collection.icon}
121+
</div>
122+
123+
<Stack gap="xs" style={{ flex: 1 }}>
124+
<Text fw={500} size="lg">
125+
{collection.title}
126+
</Text>
127+
<Text c="dimmed" size="sm">
128+
{collection.description}
129+
</Text>
130+
</Stack>
131+
132+
<Button
133+
variant="light"
134+
color={collection.color}
135+
onClick={() => handleExploreCollection(collection.query)}
136+
fullWidth
137+
>
138+
Explore
139+
</Button>
140+
</Stack>
141+
</Card>
142+
))}
143+
</SimpleGrid>
144+
</Stack>
145+
</Container>
146+
);
147+
};
148+
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
/**
2+
* Random Explorer Component
3+
*
4+
* Provides "Surprise Me" functionality to discover random entities.
5+
* Fetches a random work from OpenAlex and navigates to its detail page.
6+
*/
7+
8+
import { cachedOpenAlex } from '@bibgraph/client';
9+
import { logger } from '@bibgraph/utils';
10+
import { Button, Container, Group, Stack, Text, Title } from '@mantine/core';
11+
import { notifications } from '@mantine/notifications';
12+
import { IconRefresh, IconSparkles } from '@tabler/icons-react';
13+
import { useQuery } from '@tanstack/react-query';
14+
import { useNavigate } from '@tanstack/react-router';
15+
import { useState } from 'react';
16+
17+
import { ICON_SIZE } from '@/config/style-constants';
18+
19+
interface RandomWork {
20+
id: string;
21+
title: string;
22+
type: string;
23+
}
24+
25+
const fetchRandomWork = async (): Promise<RandomWork> => {
26+
try {
27+
// Use a random page to get different results each time
28+
const randomPage = Math.floor(Math.random() * 1000) + 1;
29+
const results = await cachedOpenAlex.client.works.searchWorks('', {
30+
// Random filters to get variety
31+
filters: {
32+
'has-fulltext:true': true,
33+
'from-publication-year': '2020',
34+
},
35+
per_page: 50,
36+
page: randomPage,
37+
});
38+
39+
if (results.results.length === 0) {
40+
throw new Error('No works found');
41+
}
42+
43+
// Pick a random work from the results
44+
const randomIndex = Math.floor(Math.random() * results.results.length);
45+
const work = results.results[randomIndex];
46+
47+
return {
48+
id: work.id,
49+
title: work.title || 'Untitled',
50+
type: 'work',
51+
};
52+
} catch (error) {
53+
logger.error('random-explorer', 'Failed to fetch random work', { error });
54+
throw error;
55+
}
56+
};
57+
58+
export const RandomExplorer: React.FC = () => {
59+
const navigate = useNavigate();
60+
const [isFetching, setIsFetching] = useState(false);
61+
62+
const { data: randomWork, refetch } = useQuery({
63+
queryKey: ['random-work'],
64+
queryFn: fetchRandomWork,
65+
enabled: false, // Don't fetch automatically
66+
retry: false,
67+
});
68+
69+
const handleSurpriseMe = async () => {
70+
setIsFetching(true);
71+
try {
72+
const result = await refetch();
73+
if (result.data) {
74+
navigate({
75+
to: `/works/${result.data.id.replace('https://openalex.org/', '')}`,
76+
});
77+
}
78+
} catch {
79+
notifications.show({
80+
title: 'Failed to load random work',
81+
message: 'Please try again',
82+
color: 'red',
83+
});
84+
} finally {
85+
setIsFetching(false);
86+
}
87+
};
88+
89+
const handleRefreshRandom = async () => {
90+
setIsFetching(true);
91+
try {
92+
await refetch();
93+
notifications.show({
94+
title: 'New random work loaded',
95+
message: randomWork ? randomWork.title : 'Click Surprise Me to explore',
96+
color: 'green',
97+
});
98+
} catch {
99+
notifications.show({
100+
title: 'Failed to load random work',
101+
message: 'Please try again',
102+
color: 'red',
103+
});
104+
} finally {
105+
setIsFetching(false);
106+
}
107+
};
108+
109+
return (
110+
<Container size="xl">
111+
<Stack gap="md">
112+
<div>
113+
<Title order={3}>Random Explorer</Title>
114+
<Text c="dimmed">Discover something new by chance</Text>
115+
</div>
116+
117+
<Group gap="md">
118+
<Button
119+
leftSection={<IconSparkles size={ICON_SIZE.SM} />}
120+
onClick={handleSurpriseMe}
121+
loading={isFetching}
122+
size="lg"
123+
variant="gradient"
124+
gradient={{ from: 'blue', to: 'cyan' }}
125+
>
126+
Surprise Me
127+
</Button>
128+
129+
<Button
130+
leftSection={<IconRefresh size={ICON_SIZE.SM} />}
131+
onClick={handleRefreshRandom}
132+
loading={isFetching}
133+
variant="light"
134+
>
135+
Load New Random
136+
</Button>
137+
138+
{randomWork && (
139+
<Text c="dimmed" size="sm" style={{ flex: 1 }}>
140+
Ready: <Text span inherit fw={500}>{randomWork.title}</Text>
141+
</Text>
142+
)}
143+
</Group>
144+
145+
<Text c="dimmed" size="sm">
146+
Click "Surprise Me" to navigate to a randomly selected research work, or "Load New Random" to preview first.
147+
</Text>
148+
</Stack>
149+
</Container>
150+
);
151+
};
152+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
.topicButton {
2+
padding: rem(4px) rem(12px);
3+
border-radius: rem(20px);
4+
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-8));
5+
border: 1px solid light-dark(var(--mantine-color-gray-3), var(--mantine-color-dark-4));
6+
transition: all 150ms ease;
7+
cursor: pointer;
8+
}
9+
10+
.topicButton:hover {
11+
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-7));
12+
border-color: light-dark(var(--mantine-color-gray-4), var(--mantine-color-dark-3));
13+
transform: translateY(-1px);
14+
}
15+
16+
.topicButton:active {
17+
transform: translateY(0);
18+
}

0 commit comments

Comments
 (0)