Skip to content

Commit da0aa8d

Browse files
authored
Merge pull request #9 from FiQCI/blog_page
Add blogs page
2 parents ec349fe + ef58cd8 commit da0aa8d

File tree

11 files changed

+321
-35
lines changed

11 files changed

+321
-35
lines changed

content/_publications/2024-08-29-Topology.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,20 @@ tags:
1111
- Helmi
1212
- Quantum
1313
type: 'Blog'
14+
filters:
15+
Skill level:
16+
- name: "Advanced"
17+
value: false
18+
- name: "Beginner"
19+
value: true
20+
Type:
21+
- name: "Blog"
22+
value: false
23+
- name: "Instructions"
24+
value: true
25+
- name: "News"
26+
value: false
27+
Theme: "Technical"
1428
---
1529

1630
*As quantum computing continues to advance, the architecture of quantum processing units (QPUs) is becoming increasingly critical to the performance of quantum circuits, the backbone of all quantum algorithms. A key factor influencing the design of these circuits is qubit connectivity—how qubits are arranged and interact within a QPU. Different quantum platforms exhibit unique connectivity patterns that directly impact how quantum circuits are implemented and optimized.*

content/assets/images/LUMI.jpg

45.4 KB
Loading

content/pages/publications.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
layout: page
33
title: Posts and publications
44
subtitle: Blog posts, publications, and other material of interest
5+
react_source_files:
6+
- blogs.js
57
---
68

7-
{% include post-list.html posts=site.publications %}
9+
{%- include react/root.html id='blogs' -%}

content/site.js

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,21 @@
1010
"title": "{{ publication.title }}",
1111
"url": "{{ publication.url | relative_url }}",
1212
"date": "{{ publication.date | date: '%-d.%-m.%Y' }}",
13-
"teaser": "{{publication.header.teaser | relative_url}}"
13+
"teaser": "{{publication.header.teaser | relative_url}}",
14+
"filters": {
15+
{%- for category in publication.filters %}
16+
{%- if category[0] == "Theme" -%}
17+
"Theme": "{{ category[1] }}"
18+
{%- else -%}
19+
"{{ category[0] }}": {
20+
{%- for option in category[1] %}
21+
"{{ option.name }}": {{ option.value | jsonify }}{%- unless forloop.last -%},{%- endunless -%}
22+
{%- endfor %}
23+
}
24+
{%- endif -%}
25+
{%- unless forloop.last -%},{%- endunless -%}
26+
{%- endfor %}
27+
}
1428
}{%- unless forloop.last == true -%},{%- endunless -%}
1529
{% endfor %}
1630
]
@@ -39,7 +53,7 @@
3953
{%- endif -%}
4054
{%- unless forloop.last -%},{%- endunless -%}
4155
{%- endfor %}
42-
}
56+
}
4357
}{%- unless forloop.last -%},{%- endunless -%}
4458
{% endfor %}
4559
]

src/components/Banner.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ export const Banner = ({ title }) => {
66
<div className='justify-start landscape:justify-start sm:justify-start md:justify-start bg-[url(/assets/images/FiQCI-banner.jpg)] bg-center w-full h-[250px] flex flex-row items-center'>
77
<div className='mx-8 lg:mx-[100px]'>
88
<div className='bg-[#0D2B53] w-fit font-bold text-white leading-tight'>
9-
<h1 className='text-5xl px-5 py-4 sm:text-4xl sm:px-6 sm:py-5 md:text-4xl md:px-10 md:py-7'>{title}</h1>
9+
<h1 className='text-4xl px-5 py-4 sm:text-4xl sm:px-6 sm:py-5 md:text-4xl md:px-10 md:py-7'>{title}</h1>
1010
</div>
1111
</div>
1212
</div>

src/components/BlogCards.jsx

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,20 @@ import { CCard, CIcon, CCardContent } from '@cscfi/csc-ui-react';
44
import { mdiArrowRight } from '@mdi/js';
55

66
const BlogCardComponent = props => {
7+
const type = props?.filters?.Type ? Object.entries(props?.filters?.Type)?.filter(field => field[1])[0][0] : "News";
78
return (
8-
<CCard className="flex flex-auto flex-col flex-wrap border border-gray-200 rounded-none shadow-md overflow-hidden hover:shadow-lg p-0 m-0 w-full"> {/* Adjusted card width */}
9+
<CCard className="flex flex-auto flex-col flex-wrap border border-gray-200 rounded-none shadow-md overflow-hidden hover:shadow-lg p-0 m-0 w-full"> {/* Adjusted card width */}
910
<img src={props.teaser} alt="Logo" className="w-full h-28 scale-125 object-cover m-0 p-0" /> {/* Reduced image height */}
1011
<CCardContent className="flex flex-col border-none m-0">
1112
<div>
1213
<a
13-
href={props.url}
14+
href={props.url.split(".")[0]}
1415
className="text-md text-black-500 hover:underline font-bold"
1516
>
16-
{props.title}
17+
{props.title.length >= 89 ? props.title.slice(0, 90) + "..." : props.title}
1718
</a>
1819
<p className="text-sm text-gray-500 pb-2 pt-1">
19-
{props.type} | {props.date}
20+
{ type } | {props.date}
2021
</p>
2122
</div>
2223
</CCardContent>
@@ -26,7 +27,7 @@ const BlogCardComponent = props => {
2627

2728

2829

29-
export const BlogCard = () => {
30+
const BlogCard = () => {
3031
return (
3132
<div className="mx-8 lg:mx-[100px] py-6">
3233
<div className="flex items-center justify-between mb-6">
@@ -37,7 +38,7 @@ export const BlogCard = () => {
3738
{ SITE.publications.slice(-5).reverse().map(blog => <BlogCardComponent {...blog} />) }
3839
</div>
3940
<div className="mt-4">
40-
<a
41+
<a
4142
href="#"
4243
className="text-sky-800 hover:underline font-bold"
4344
>
@@ -50,3 +51,5 @@ export const BlogCard = () => {
5051
</div>
5152
);
5253
};
54+
55+
export { BlogCard, BlogCardComponent }

src/components/Blogs.jsx

Lines changed: 236 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,236 @@
1+
import React, { useState, useEffect, useCallback } from 'react';
2+
import '@cscfi/csc-ui-react/css/theme.css';
3+
import {
4+
CPagination, CCheckbox, CSelect, CButton, CModal, CCard,
5+
CCardTitle, CCardContent, CCardActions
6+
} from '@cscfi/csc-ui-react';
7+
import { BlogCardComponent } from './BlogCards';
8+
9+
const BlogFilters = ({ filters, handleFilterChange }) => {
10+
const handleCheckboxChange = useCallback((category, option) => {
11+
handleFilterChange({
12+
...filters,
13+
[category]: { ...filters[category], [option]: !filters[category][option] } //toggle the state
14+
});
15+
}, [filters, handleFilterChange]);
16+
17+
const handleChangeTheme = useCallback(selectedTheme => {
18+
handleFilterChange({ ...filters, Theme: selectedTheme.detail || '' }); //update theme or set to ''
19+
}, [filters, handleFilterChange]);
20+
21+
return (
22+
<div className='flex flex-col gap-2'>
23+
{Object.entries(filters).slice(0, -1).map(([category, options]) => ( //slice(0,-1) to exclude Theme
24+
<FilterCategory
25+
key={category}
26+
category={category}
27+
options={options}
28+
handleCheckboxChange={handleCheckboxChange}
29+
/>
30+
))}
31+
<FilterTheme
32+
selectedTheme={filters.Theme}
33+
handleChangeTheme={handleChangeTheme}
34+
/>
35+
</div>
36+
);
37+
};
38+
39+
//Checkbox filters
40+
const FilterCategory = ({ category, options, handleCheckboxChange }) => (
41+
<div>
42+
<h3 className='font-bold'>{category}</h3>
43+
{Object.keys(options).map(option => ( //generate a chekcbox for each filter category
44+
<CCheckbox
45+
hideDetails={true}
46+
key={option}
47+
checked={options[option]}
48+
onChangeValue={() => handleCheckboxChange(category, option)}
49+
>
50+
<p className='text-sm'>{option}</p>
51+
</CCheckbox>
52+
))}
53+
</div>
54+
);
55+
56+
//Theme filter
57+
const FilterTheme = ({ selectedTheme, handleChangeTheme }) => (
58+
<div>
59+
<p className='font-bold'>Theme</p>
60+
<CSelect
61+
hideDetails={true}
62+
className='py-2'
63+
clearable
64+
value={selectedTheme}
65+
items={[
66+
{ name: 'Hybrid QC+HPC computing', value: 'hybrid QC+HPC computing' },
67+
{ name: 'Programming', value: 'programming' },
68+
{ name: 'Algorithm', value: 'algorithm' },
69+
{ name: 'Technical', value: 'Technical' },
70+
]}
71+
placeholder='Choose a theme'
72+
onChangeValue={handleChangeTheme}
73+
/>
74+
</div>
75+
);
76+
77+
//Modal filter for mobile
78+
const FilterModal = ({ isModalOpen, setIsModalOpen, filters, handleFilterChange }) => {
79+
return (
80+
<CModal
81+
key={isModalOpen ? 'open' : 'closed'}
82+
style={{ overflow: 'scroll' }}
83+
className='overflow-scroll'
84+
value={isModalOpen}
85+
dismissable
86+
onChangeValue={event => setIsModalOpen(event.detail)}
87+
>
88+
<CCard style={{ overflow: 'scroll' }} className='overflow-scroll max-h-[80vh]'>
89+
<CCardTitle>Filters</CCardTitle>
90+
<CCardContent>
91+
<BlogFilters filters={filters} handleFilterChange={handleFilterChange} />
92+
</CCardContent>
93+
<CCardActions justify='end'>
94+
<CButton onClick={() => setIsModalOpen(false)} text>Close</CButton>
95+
</CCardActions>
96+
</CCard>
97+
</CModal>
98+
);
99+
};
100+
101+
//List blogs in a grid with pagination
102+
const BlogsList = ({ title, blogs, paginationOptions, handlePageChange, showFilters, onOpenDialog }) => (
103+
<div>
104+
<div className='flex flex-row justify-between'>
105+
<h2 className='text-3xl font-bold'>{title}</h2>
106+
{showFilters && //to not show the button on every EventsList instance
107+
<CButton
108+
className='flex items-center py-2 lg:hidden'
109+
onClick={() => onOpenDialog()}
110+
>
111+
Filters
112+
</CButton>
113+
}
114+
</div>
115+
{blogs.length ? (
116+
<>
117+
<div className='grid grid-cols-1 py-6 sm:grid-cols-3 md:grid-cols-3 lg:grid-cols-3 xl:grid-cols-4 gap-6'>
118+
{blogs.slice(
119+
(paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage,
120+
(paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage + paginationOptions.itemsPerPage
121+
).map(blog => (
122+
<BlogCardComponent key={blog.id} {...blog} />
123+
))}
124+
</div>
125+
<CPagination
126+
value={paginationOptions}
127+
hideDetails
128+
onChangeValue={handlePageChange}
129+
control
130+
/>
131+
</>
132+
) : (
133+
<p className='pt-6 pb-8'>No {title.toLowerCase()}.</p>
134+
)}
135+
</div>
136+
);
137+
138+
//Full blogs component
139+
export const Blogs = () => {
140+
const blogs_dict = SITE.publications; //get blogs
141+
const [isModalOpen, setIsModalOpen] = useState(false); //modal control
142+
const [filters, setFilters] = useState({
143+
"Skill level": { "Advanced": false, "Beginner": false },
144+
"Type": { "Blog": false, "Instructions": false, "News": false },
145+
"Theme": "",
146+
}); //filter state
147+
148+
const [options, setOptions] = useState({
149+
itemCount: blogs_dict.length,
150+
itemsPerPage: 8,
151+
currentPage: 1,
152+
pageSizes: [5, 10, 15, 25, 50]
153+
}); //pagination control
154+
155+
const [filteredBlogs, setFilteredBlogs] = useState(blogs_dict);
156+
157+
useEffect(() => {
158+
document.body.classList.add("min-w-fit");
159+
}, []);
160+
161+
useEffect(() => {
162+
document.body.style.overflow = isModalOpen ? 'hidden' : 'visible';
163+
return () => {
164+
document.body.style.overflow = 'visible';
165+
};
166+
}, [isModalOpen]);
167+
168+
useEffect(() => { //set filteredBlogs everytime filters changes
169+
const applyFilters = (blog) => {
170+
if (filters.Theme && blog?.filters?.Theme !== filters.Theme) {
171+
return false;
172+
}
173+
// For every other filter category...
174+
return Object.entries(filters).every(([category, options]) => {
175+
// Skip the "Theme" category here
176+
if (category === "Theme") return true;
177+
178+
// Create an array of only the options that are checked (active)
179+
const activeOptions = Object.entries(options).filter(([_, checked]) => checked);
180+
181+
// If no options are active in this category, do not filter out the event:
182+
if (activeOptions.length === 0) return true;
183+
184+
// Otherwise, require that at least one active option is true in the event:
185+
return activeOptions.some(([option]) => blog?.filters?.[category]?.[option]);
186+
});
187+
};
188+
189+
//apply filter
190+
const filtered = blogs_dict.filter(applyFilters);
191+
setFilteredBlogs(filtered);
192+
193+
//also update item count in pagination options
194+
setOptions(prev => ({ ...prev, itemCount: filtered.length }));
195+
}, [filters]);
196+
197+
const onOpenDialog = () => { //modal control
198+
setIsModalOpen(true);
199+
};
200+
201+
const handlePageChange = (setOptions) => (blog) => {
202+
// blog.detail.currentPage should be the new page number.
203+
setOptions(prev => ({ ...prev, currentPage: blog.detail.currentPage }));
204+
};
205+
206+
const handleFilterChange = (newFilters) => {
207+
setFilters(newFilters);
208+
setOptions(prev => ({ ...prev, currentPage: 1 }));
209+
};
210+
211+
return (
212+
<div className='flex flex-col items-top mb-2'>
213+
<div className='mt-8 mx-8 lg:mx-[100px] flex lg:grid grid-cols-5 gap-8'>
214+
<div className='hidden lg:block lg:sticky lg:top-16 lg:self-start z-10'>
215+
<BlogFilters filters={filters} handleFilterChange={handleFilterChange} />
216+
</div>
217+
<div className='md:py-0 col-span-4'>
218+
<BlogsList
219+
title='Blogs'
220+
blogs={[...filteredBlogs].reverse()}
221+
paginationOptions={options}
222+
handlePageChange={handlePageChange(setOptions)}
223+
showFilters={true}
224+
onOpenDialog={onOpenDialog}
225+
/>
226+
</div>
227+
</div>
228+
<FilterModal
229+
isModalOpen={isModalOpen}
230+
setIsModalOpen={setIsModalOpen}
231+
filters={filters}
232+
handleFilterChange={handleFilterChange}
233+
/>
234+
</div>
235+
);
236+
};

0 commit comments

Comments
 (0)