diff --git a/content/_publications/2024-08-29-Topology.md b/content/_publications/2024-08-29-Topology.md index cfb066c51be7..52852673baf3 100644 --- a/content/_publications/2024-08-29-Topology.md +++ b/content/_publications/2024-08-29-Topology.md @@ -11,6 +11,20 @@ tags: - Helmi - Quantum type: 'Blog' +filters: + Skill level: + - name: "Advanced" + value: false + - name: "Beginner" + value: true + Type: + - name: "Blog" + value: false + - name: "Instructions" + value: true + - name: "News" + value: false + Theme: "Technical" --- *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.* diff --git a/content/assets/images/LUMI.jpg b/content/assets/images/LUMI.jpg new file mode 100644 index 000000000000..549cc2cab230 Binary files /dev/null and b/content/assets/images/LUMI.jpg differ diff --git a/content/pages/publications.md b/content/pages/publications.md index 0ed61b3f5c3c..b016fb3b9441 100644 --- a/content/pages/publications.md +++ b/content/pages/publications.md @@ -2,6 +2,8 @@ layout: page title: Posts and publications subtitle: Blog posts, publications, and other material of interest +react_source_files: + - blogs.js --- -{% include post-list.html posts=site.publications %} +{%- include react/root.html id='blogs' -%} diff --git a/content/site.js b/content/site.js index f9838808f6e6..80834078b0fb 100644 --- a/content/site.js +++ b/content/site.js @@ -10,7 +10,21 @@ "title": "{{ publication.title }}", "url": "{{ publication.url | relative_url }}", "date": "{{ publication.date | date: '%-d.%-m.%Y' }}", - "teaser": "{{publication.header.teaser | relative_url}}" + "teaser": "{{publication.header.teaser | relative_url}}", + "filters": { + {%- for category in publication.filters %} + {%- if category[0] == "Theme" -%} + "Theme": "{{ category[1] }}" + {%- else -%} + "{{ category[0] }}": { + {%- for option in category[1] %} + "{{ option.name }}": {{ option.value | jsonify }}{%- unless forloop.last -%},{%- endunless -%} + {%- endfor %} + } + {%- endif -%} + {%- unless forloop.last -%},{%- endunless -%} + {%- endfor %} + } }{%- unless forloop.last == true -%},{%- endunless -%} {% endfor %} ] @@ -39,7 +53,7 @@ {%- endif -%} {%- unless forloop.last -%},{%- endunless -%} {%- endfor %} - } + } }{%- unless forloop.last -%},{%- endunless -%} {% endfor %} ] diff --git a/src/components/Banner.jsx b/src/components/Banner.jsx index 6cfb2ec83400..e61d0214ebf4 100644 --- a/src/components/Banner.jsx +++ b/src/components/Banner.jsx @@ -6,7 +6,7 @@ export const Banner = ({ title }) => {
-

{title}

+

{title}

diff --git a/src/components/BlogCards.jsx b/src/components/BlogCards.jsx index eb5d71cfd0ba..fde951a7491e 100644 --- a/src/components/BlogCards.jsx +++ b/src/components/BlogCards.jsx @@ -4,19 +4,20 @@ import { CCard, CIcon, CCardContent } from '@cscfi/csc-ui-react'; import { mdiArrowRight } from '@mdi/js'; const BlogCardComponent = props => { + const type = props?.filters?.Type ? Object.entries(props?.filters?.Type)?.filter(field => field[1])[0][0] : "News"; return ( - {/* Adjusted card width */} + {/* Adjusted card width */} Logo {/* Reduced image height */}
- {props.title} + {props.title.length >= 89 ? props.title.slice(0, 90) + "..." : props.title}

- {props.type} | {props.date} + { type } | {props.date}

@@ -26,7 +27,7 @@ const BlogCardComponent = props => { -export const BlogCard = () => { +const BlogCard = () => { return (
@@ -37,7 +38,7 @@ export const BlogCard = () => { { SITE.publications.slice(-5).reverse().map(blog => ) }
- @@ -50,3 +51,5 @@ export const BlogCard = () => {
); }; + +export { BlogCard, BlogCardComponent } \ No newline at end of file diff --git a/src/components/Blogs.jsx b/src/components/Blogs.jsx new file mode 100644 index 000000000000..a13c3cf33fe4 --- /dev/null +++ b/src/components/Blogs.jsx @@ -0,0 +1,236 @@ +import React, { useState, useEffect, useCallback } from 'react'; +import '@cscfi/csc-ui-react/css/theme.css'; +import { + CPagination, CCheckbox, CSelect, CButton, CModal, CCard, + CCardTitle, CCardContent, CCardActions +} from '@cscfi/csc-ui-react'; +import { BlogCardComponent } from './BlogCards'; + +const BlogFilters = ({ filters, handleFilterChange }) => { + const handleCheckboxChange = useCallback((category, option) => { + handleFilterChange({ + ...filters, + [category]: { ...filters[category], [option]: !filters[category][option] } //toggle the state + }); + }, [filters, handleFilterChange]); + + const handleChangeTheme = useCallback(selectedTheme => { + handleFilterChange({ ...filters, Theme: selectedTheme.detail || '' }); //update theme or set to '' + }, [filters, handleFilterChange]); + + return ( +
+ {Object.entries(filters).slice(0, -1).map(([category, options]) => ( //slice(0,-1) to exclude Theme + + ))} + +
+ ); +}; + +//Checkbox filters +const FilterCategory = ({ category, options, handleCheckboxChange }) => ( +
+

{category}

+ {Object.keys(options).map(option => ( //generate a chekcbox for each filter category + handleCheckboxChange(category, option)} + > +

{option}

+
+ ))} +
+); + +//Theme filter +const FilterTheme = ({ selectedTheme, handleChangeTheme }) => ( +
+

Theme

+ +
+); + +//Modal filter for mobile +const FilterModal = ({ isModalOpen, setIsModalOpen, filters, handleFilterChange }) => { + return ( + setIsModalOpen(event.detail)} + > + + Filters + + + + + setIsModalOpen(false)} text>Close + + + + ); +}; + +//List blogs in a grid with pagination +const BlogsList = ({ title, blogs, paginationOptions, handlePageChange, showFilters, onOpenDialog }) => ( +
+
+

{title}

+ {showFilters && //to not show the button on every EventsList instance + onOpenDialog()} + > + Filters + + } +
+ {blogs.length ? ( + <> +
+ {blogs.slice( + (paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage, + (paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage + paginationOptions.itemsPerPage + ).map(blog => ( + + ))} +
+ + + ) : ( +

No {title.toLowerCase()}.

+ )} +
+); + +//Full blogs component +export const Blogs = () => { + const blogs_dict = SITE.publications; //get blogs + const [isModalOpen, setIsModalOpen] = useState(false); //modal control + const [filters, setFilters] = useState({ + "Skill level": { "Advanced": false, "Beginner": false }, + "Type": { "Blog": false, "Instructions": false, "News": false }, + "Theme": "", + }); //filter state + + const [options, setOptions] = useState({ + itemCount: blogs_dict.length, + itemsPerPage: 8, + currentPage: 1, + pageSizes: [5, 10, 15, 25, 50] + }); //pagination control + + const [filteredBlogs, setFilteredBlogs] = useState(blogs_dict); + + useEffect(() => { + document.body.classList.add("min-w-fit"); + }, []); + + useEffect(() => { + document.body.style.overflow = isModalOpen ? 'hidden' : 'visible'; + return () => { + document.body.style.overflow = 'visible'; + }; + }, [isModalOpen]); + + useEffect(() => { //set filteredBlogs everytime filters changes + const applyFilters = (blog) => { + if (filters.Theme && blog?.filters?.Theme !== filters.Theme) { + return false; + } + // For every other filter category... + return Object.entries(filters).every(([category, options]) => { + // Skip the "Theme" category here + if (category === "Theme") return true; + + // Create an array of only the options that are checked (active) + const activeOptions = Object.entries(options).filter(([_, checked]) => checked); + + // If no options are active in this category, do not filter out the event: + if (activeOptions.length === 0) return true; + + // Otherwise, require that at least one active option is true in the event: + return activeOptions.some(([option]) => blog?.filters?.[category]?.[option]); + }); + }; + + //apply filter + const filtered = blogs_dict.filter(applyFilters); + setFilteredBlogs(filtered); + + //also update item count in pagination options + setOptions(prev => ({ ...prev, itemCount: filtered.length })); + }, [filters]); + + const onOpenDialog = () => { //modal control + setIsModalOpen(true); + }; + + const handlePageChange = (setOptions) => (blog) => { + // blog.detail.currentPage should be the new page number. + setOptions(prev => ({ ...prev, currentPage: blog.detail.currentPage })); + }; + + const handleFilterChange = (newFilters) => { + setFilters(newFilters); + setOptions(prev => ({ ...prev, currentPage: 1 })); + }; + + return ( +
+
+
+ +
+
+ +
+
+ +
+ ); +}; diff --git a/src/components/Events.jsx b/src/components/Events.jsx index a6f7c5e231b1..cdf071040add 100644 --- a/src/components/Events.jsx +++ b/src/components/Events.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo, useCallback } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import '@cscfi/csc-ui-react/css/theme.css'; import { CPagination, CCheckbox, CSelect, CButton, CModal, CCard, @@ -53,6 +53,7 @@ const FilterCategory = ({ category, options, handleCheckboxChange }) => (

{category}

{Object.keys(options).map(option => ( //generate a chekcbox for each filter category (

Theme

setIsModalOpen(event.detail)} > - + Filters - ) + ); }; //List events in a grid with pagination @@ -129,12 +132,12 @@ const EventsList = ({ title, events, paginationOptions, handlePageChange, showFi {events.length ? ( <>
- {events.slice((paginationOptions.currentPage - 1) * - paginationOptions.itemsPerPage, (paginationOptions.currentPage - 1) * - paginationOptions.itemsPerPage + paginationOptions.itemsPerPage) - .map(event => - - )} + {events.slice( + (paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage, + (paginationOptions.currentPage - 1) * paginationOptions.itemsPerPage + paginationOptions.itemsPerPage + ).map(event => ( + + ))}
{ const events_dict = SplitEvents(); //get events - //console.log(events_dict) const [isModalOpen, setIsModalOpen] = useState(false); //modal control const [filters, setFilters] = useState({ "Availability": { "Open to anyone": false, "Registration needed": false }, @@ -180,16 +182,11 @@ export const Events = () => { const [filteredEvents, setFilteredEvents] = useState(events_dict); useEffect(() => { - document.body.classList.add("min-w-fit") - }) + document.body.classList.add("min-w-fit"); + }, []); useEffect(() => { - if (isModalOpen) { - document.body.style.overflow = 'hidden'; - } else { - document.body.style.overflow = 'visible'; - } - // Always clean up on unmount + document.body.style.overflow = isModalOpen ? 'hidden' : 'visible'; return () => { document.body.style.overflow = 'visible'; }; @@ -235,7 +232,7 @@ export const Events = () => { }; const handlePageChange = (setOptions) => (event) => { - // event.detail should be the new page number. + // event.detail.currentPage should be the new page number. setOptions(prev => ({ ...prev, currentPage: event.detail.currentPage })); }; @@ -248,7 +245,7 @@ export const Events = () => { return (
-
+
@@ -257,7 +254,8 @@ export const Events = () => { events={[...filteredEvents.upcoming].reverse()} paginationOptions={optionsUpcoming} handlePageChange={handlePageChange(setOptionsUpcoming)} - showFilters={true} onOpenDialog={onOpenDialog} + showFilters={true} + onOpenDialog={onOpenDialog} /> { isModalOpen={isModalOpen} setIsModalOpen={setIsModalOpen} filters={filters} - handleFilterChange={handleFilterChange} /> + handleFilterChange={handleFilterChange} + />
); }; diff --git a/src/components/Home.jsx b/src/components/Home.jsx index 23df788c0c07..0d0652d374f6 100644 --- a/src/components/Home.jsx +++ b/src/components/Home.jsx @@ -35,7 +35,7 @@ const ContentButton = ({ text, href, icon=mdiArrowDown }) => { export const Home = () => { return (
-
+
diff --git a/src/roots/blogs.jsx b/src/roots/blogs.jsx new file mode 100644 index 000000000000..86ab2982f8e3 --- /dev/null +++ b/src/roots/blogs.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import { Blogs } from '../components/Blogs' +import { Banner } from '../components/Banner' +import { injectComponent } from '../utils/root' + +const component = ( + <> + + + ) +const rootId = 'blogs' + +injectComponent(component, rootId) diff --git a/tailwind.config.js b/tailwind.config.js index 9a9ce4de33f9..b4a5d5a3c026 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,7 +5,12 @@ module.exports = { files: ["./src/**/*.{js,jsx}", "./content/**/*.{md,html}"], }, theme: { - extend: {}, + extend: { + backgroundImage: { + // You can choose any key name; here, we use 'hero' + 'lumi': "url('/assets/images/LUMI.jpg')", + }, + }, }, plugins: [require("@tailwindcss/typography")], };