feat: implement filtering

This commit is contained in:
jinhojang6 2024-09-28 00:41:52 +09:00
parent 057908f3c3
commit 6b4a2bbc82
No known key found for this signature in database
GPG Key ID: 1762F21FE8B543F8
11 changed files with 302 additions and 28 deletions

View File

@ -14,7 +14,7 @@ const nextConfig = {
reactStrictMode: true,
pageExtensions: ['js', 'jsx', 'mdx', 'ts', 'tsx'],
images: {
domains: ['avatars.githubusercontent.com', 'source.unsplash.com'],
domains: ['avatars.githubusercontent.com', 'ordinal-operators.s3.amazonaws.com'],
},
}

View File

@ -35,6 +35,7 @@
"@types/mdx": "^2.0.8",
"@vercel/og": "^0.5.4",
"axios": "^1.4.0",
"lazysizes": "^5.3.2",
"next": "^14.2.3",
"next-mdx-remote": "^4.4.1",
"react": "18.2.0",

View File

@ -0,0 +1,59 @@
import styled from '@emotion/styled'
const CustomCheckboxWrapper = styled.label`
display: flex;
align-items: center;
cursor: pointer;
margin-bottom: 10px;
`
const HiddenCheckbox = styled.input`
position: absolute;
opacity: 0;
height: 0;
width: 0;
cursor: pointer;
`
const StyledCheckbox = styled.span<{ isChecked: boolean }>`
display: inline-block;
width: 18px;
height: 18px;
background-color: ${({ isChecked }) => (isChecked ? 'white' : 'black')};
border: 2px solid white;
position: relative;
margin-right: 10px;
&::after {
content: ${({ isChecked }) => (isChecked ? "'✓'" : "''")};
color: black;
position: absolute;
top: 0;
left: 3px;
font-size: 14px;
}
`
const LabelText = styled.span`
color: white;
`
interface CustomCheckboxProps {
checked: boolean
onChange: () => void
label: string
}
const Checkbox: React.FC<CustomCheckboxProps> = ({
checked,
onChange,
label,
}) => (
<CustomCheckboxWrapper>
<HiddenCheckbox type="checkbox" checked={checked} onChange={onChange} />
<StyledCheckbox isChecked={checked} />
<LabelText>{label}</LabelText>
</CustomCheckboxWrapper>
)
export default Checkbox

View File

@ -0,0 +1,130 @@
import styled from '@emotion/styled'
import React, { useState } from 'react'
import Checkbox from './Checkbox' // Import the CustomCheckbox
interface DropdownProps {
title: string
options: string[]
onSelectionChange: (selectedOptions: any) => void
}
const Dropdown: React.FC<DropdownProps> = ({
title,
options,
onSelectionChange,
}) => {
const [selectedOptions, setSelectedOptions] = useState<string[]>([])
const [isExpanded, setIsExpanded] = useState(false)
const handleSelect = (option: string) => {
let newSelectedOptions = []
if (selectedOptions.includes(option)) {
newSelectedOptions = selectedOptions.filter((o) => o !== option)
} else {
newSelectedOptions = [...selectedOptions, option]
}
setSelectedOptions(newSelectedOptions)
onSelectionChange(newSelectedOptions)
}
const selectAll = () => {
setSelectedOptions(options)
onSelectionChange(options)
}
const clearAll = () => {
setSelectedOptions([])
onSelectionChange([])
}
const toggleDropdown = () => {
setIsExpanded(!isExpanded)
}
return (
<DropdownContainer>
<DropdownHeader onClick={toggleDropdown}>
<span>{title}</span>
<Chevron isExpanded={isExpanded}>
<img src="/assets/chevron-down.svg" alt="chevron down" />
</Chevron>
</DropdownHeader>
{isExpanded && (
<DropdownContent>
{options.map((option, index) => (
<Checkbox
key={index}
checked={selectedOptions.includes(option)}
onChange={() => handleSelect(option)}
label={option}
/>
))}
<ButtonContainer>
<Button onClick={selectAll}>Select All</Button>
<Button onClick={clearAll}>Clear</Button>
</ButtonContainer>
</DropdownContent>
)}
</DropdownContainer>
)
}
// Additional styled components for Dropdown
const DropdownContainer = styled.div`
position: relative;
display: inline-block;
width: 200px;
`
const DropdownHeader = styled.div`
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
cursor: pointer;
border: 1px solid white;
background-color: black;
color: white;
`
const Chevron = styled.span<{ isExpanded: boolean }>`
display: inline-flex;
transition: transform 0.3s ease;
transform: ${({ isExpanded }) =>
isExpanded ? 'rotate(180deg)' : 'rotate(0deg)'};
`
const DropdownContent = styled.div`
position: absolute;
top: 100%;
left: 0;
width: 100%;
background-color: black;
border: 1px solid white;
z-index: 10;
padding: 10px;
box-sizing: border-box;
`
const ButtonContainer = styled.div`
display: flex;
margin-top: 10px;
`
const Button = styled.button`
background-color: transparent;
border: 1px solid white;
width: 100%;
color: white;
padding: 5px;
cursor: pointer;
transition: background-color 0.3s;
&:hover {
background-color: white;
color: black;
}
`
export default Dropdown

View File

@ -0,0 +1 @@
export { default as Dropdown } from './Dropdown'

View File

@ -1,6 +1,7 @@
import { breakpoints } from '@/configs/ui.configs'
import { ProcessedOperator } from '@/containers/Dashboard/DashboardContainer'
import styled from '@emotion/styled'
import 'lazysizes'
import Link from 'next/link'
import React, { useCallback, useEffect, useRef, useState } from 'react'
@ -65,8 +66,10 @@ const OperatorGrid: React.FC<OperatorGridProps> = ({ data, isLoading }) => {
<img
key={index}
src={operator.image}
data-src={operator.gif}
alt={`Operator ${index + 1}`}
loading="lazy"
className="lazyload"
/>
</GridItem>
</Link>
@ -81,8 +84,10 @@ const OperatorGrid: React.FC<OperatorGridProps> = ({ data, isLoading }) => {
<img
key={index}
src={operator.image}
data-src={operator.gif}
alt={`Operator ${index + 1}`}
loading="lazy"
className="lazyload"
/>
</GridItem>
</Link>
@ -99,6 +104,13 @@ const GridContainer = styled.section`
gap: 16px;
margin-top: 16px;
img {
aspect-ratio: 1;
object-fit: cover;
width: 100%;
height: 100%;
}
@media (max-width: ${breakpoints.md}px) {
grid-template-columns: repeat(3, 1fr);
}
@ -106,19 +118,15 @@ const GridContainer = styled.section`
@media (max-width: ${breakpoints.sm}px) {
grid-template-columns: repeat(2, 1fr);
}
img {
aspect-ratio: 1;
object-fit: contain;
width: 216px;
}
`
const GridItem = styled.div`
display: flex;
justify-content: center;
align-items: center;
overflow: hidden;
cursor: pointer;
background-color: var(--grey-900);
border-radius: 8px;
`
const Placeholder = styled.div`

View File

@ -16,6 +16,7 @@ export interface Operator {
id: number
name: string
image_400_url: string
image_400_jpeg_url: string
}
export interface Group {
@ -28,6 +29,7 @@ export interface ProcessedOperator {
id: string
image: string
name: string
gif: string
pointsPerHour: number
isStaked: boolean
isPinned: boolean
@ -39,7 +41,7 @@ const DashboardContainer: React.FC<DashboardPageProps> = ({
}) => {
const { data, isLoading } = useGetOperators()
const processedOperators = processOperators(data as Group[])
const processedOperators = processOperators(data as Group[], [])
const random20Operators = getRandomSubset(processedOperators, 20)

View File

@ -1,27 +1,60 @@
import { OperatorFilter } from '@/components/Explore/OperatorFilter'
import { Dropdown } from '@/components/Dropdown'
import { OperatorGrid } from '@/components/Explore/OperatorGrid'
import styled from '@emotion/styled'
import React from 'react'
import React, { useState } from 'react'
import useGetOperators from '../../../apis/operators/useGetOperators'
import { Archetype } from '../../../types/operators'
import { processOperators } from '../../../utils/operators'
interface ExploreSectionProps {}
// one of Archetype
const archetypes: Archetype[] = [
'Alchemist',
'Artisan',
'Explorer',
'Illuminator',
'Magician',
'Memetic',
'Oracle',
'Outlaw',
'Philosopher',
'Polymath',
]
const ExploreSection: React.FC<ExploreSectionProps> = () => {
const { data, isLoading } = useGetOperators()
const processedOperators = processOperators(data as any)
const [selectedArchetypes, setSelectedArchetypes] = useState<Archetype[]>([])
const processedOperators = processOperators(data as any, selectedArchetypes)
const handleSelectionChange = (selectedOptions: Archetype[]) => {
setSelectedArchetypes(selectedOptions)
}
return (
<StyledExploreSection>
<Container>
<h1 className="section-title">Explore Operators</h1>
<OperatorFilter />
<OperatorGrid data={processedOperators as any} isLoading={isLoading} />
</StyledExploreSection>
<DropdownContainer>
<Dropdown
title="Archetype"
options={archetypes}
onSelectionChange={handleSelectionChange}
/>
</DropdownContainer>
<OperatorGrid
key={selectedArchetypes.join(',')}
data={processedOperators as any}
isLoading={isLoading}
/>
</Container>
)
}
const StyledExploreSection = styled.main`
const Container = styled.main`
display: flex;
flex-direction: column;
min-height: 100vh;
overflow: hidden;
.section-title {
@ -32,4 +65,11 @@ const StyledExploreSection = styled.main`
}
`
const DropdownContainer = styled.div`
display: flex;
justify-content: center;
align-items: center;
margin-top: 70px;
`
export default ExploreSection

11
types/operators.ts Normal file
View File

@ -0,0 +1,11 @@
export type Archetype =
| 'Alchemist'
| 'Artisan'
| 'Explorer'
| 'Illuminator'
| 'Magician'
| 'Memetic'
| 'Oracle'
| 'Outlaw'
| 'Philosopher'
| 'Polymath'

View File

@ -2,18 +2,35 @@ import {
Group,
ProcessedOperator,
} from '@/containers/Dashboard/DashboardContainer'
import { Archetype } from '../types/operators'
export function processOperators(data: Group[]): ProcessedOperator[] {
return data?.flatMap((group) =>
group.operators.map((operator) => ({
id: operator.id.toString(), // Convert ID to string
image: operator.image_400_url,
name: `OP ${operator.id}`,
pointsPerHour: Math.floor(Math.random() * 500), // Random value for points per hour
isStaked: false,
isPinned: false,
})),
)
export function processOperators(
data: Group[],
selectedArchetypes: Archetype[],
): ProcessedOperator[] {
const hasSelectedArchetypes = selectedArchetypes.length > 0
return data?.flatMap((group) => {
const groupArchetype = group.name.slice(0, -1) as Archetype
const isSelectedArchetype = hasSelectedArchetypes
? selectedArchetypes.includes(groupArchetype)
: true
if (isSelectedArchetype) {
return group.operators.map((operator) => ({
id: operator.id.toString(),
image: operator.image_400_jpeg_url,
gif: operator.image_400_url,
name: `OP ${operator.id}`,
pointsPerHour: Math.floor(Math.random() * 500),
isStaked: false,
isPinned: false,
}))
}
return []
})
}
export function getRandomSubset<T>(array: T[], count: number): T[] {

View File

@ -2628,6 +2628,11 @@ language-tags@=1.0.5:
dependencies:
language-subtag-registry "~0.3.2"
lazysizes@^5.3.2:
version "5.3.2"
resolved "https://registry.yarnpkg.com/lazysizes/-/lazysizes-5.3.2.tgz#27f974c26f5fcc33e7db765c0f4930488c8a2984"
integrity sha512-22UzWP+Vedi/sMeOr8O7FWimRVtiNJV2HCa+V8+peZOw6QbswN9k58VUhd7i6iK5bw5QkYrF01LJbeJe0PV8jg==
levn@^0.4.1:
version "0.4.1"
resolved "https://registry.yarnpkg.com/levn/-/levn-0.4.1.tgz#ae4562c007473b932a6200d403268dd2fffc6ade"