|
| 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